Update voice.py, vlib, tests, bundles
Browse files- tests/_helpers.py +13 -0
- tests/test_units.py +707 -1
- vlib/cache.py +29 -1
- vlib/delta.py +65 -3
- voice-cli.tar.gz +2 -2
- voice.py +1 -1
- voice.pyz +2 -2
tests/_helpers.py
CHANGED
|
@@ -91,6 +91,9 @@ class IsolatedVoices(unittest.TestCase):
|
|
| 91 |
self._old = voice.VOICES_DIR
|
| 92 |
self.vdir = Path(tempfile.mkdtemp(prefix="voices-"))
|
| 93 |
voice.VOICES_DIR = self.vdir
|
|
|
|
|
|
|
|
|
|
| 94 |
# registry + reports + parts all derive from VOICES_DIR at call time,
|
| 95 |
# except these module constants: repoint them too.
|
| 96 |
self._old_reports = (voice.REPORTS_JSONL, voice.REPORTS_OUTBOX, voice.OPERATIONS_LOG,
|
|
@@ -99,11 +102,21 @@ class IsolatedVoices(unittest.TestCase):
|
|
| 99 |
voice.REPORTS_OUTBOX = self.vdir / "outbox"
|
| 100 |
voice.OPERATIONS_LOG = self.vdir / "operations.log"
|
| 101 |
voice.HF_TOKEN_PATH = self.vdir / ".hf_token"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
def tearDown(self):
|
| 104 |
voice.VOICES_DIR = self._old
|
| 105 |
(voice.REPORTS_JSONL, voice.REPORTS_OUTBOX, voice.OPERATIONS_LOG,
|
| 106 |
voice.HF_TOKEN_PATH) = self._old_reports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
def ns(**kw):
|
|
|
|
| 91 |
self._old = voice.VOICES_DIR
|
| 92 |
self.vdir = Path(tempfile.mkdtemp(prefix="voices-"))
|
| 93 |
voice.VOICES_DIR = self.vdir
|
| 94 |
+
voice_ctx = sys.modules.get("vlib.ctx")
|
| 95 |
+
if voice_ctx is not None:
|
| 96 |
+
voice_ctx.VOICES_DIR = self.vdir
|
| 97 |
# registry + reports + parts all derive from VOICES_DIR at call time,
|
| 98 |
# except these module constants: repoint them too.
|
| 99 |
self._old_reports = (voice.REPORTS_JSONL, voice.REPORTS_OUTBOX, voice.OPERATIONS_LOG,
|
|
|
|
| 102 |
voice.REPORTS_OUTBOX = self.vdir / "outbox"
|
| 103 |
voice.OPERATIONS_LOG = self.vdir / "operations.log"
|
| 104 |
voice.HF_TOKEN_PATH = self.vdir / ".hf_token"
|
| 105 |
+
if voice_ctx is not None:
|
| 106 |
+
voice_ctx.REPORTS_JSONL = voice.REPORTS_JSONL
|
| 107 |
+
voice_ctx.REPORTS_OUTBOX = voice.REPORTS_OUTBOX
|
| 108 |
+
voice_ctx.OPERATIONS_LOG = voice.OPERATIONS_LOG
|
| 109 |
+
voice_ctx.HF_TOKEN_PATH = voice.HF_TOKEN_PATH
|
| 110 |
|
| 111 |
def tearDown(self):
|
| 112 |
voice.VOICES_DIR = self._old
|
| 113 |
(voice.REPORTS_JSONL, voice.REPORTS_OUTBOX, voice.OPERATIONS_LOG,
|
| 114 |
voice.HF_TOKEN_PATH) = self._old_reports
|
| 115 |
+
voice_ctx = sys.modules.get("vlib.ctx")
|
| 116 |
+
if voice_ctx is not None:
|
| 117 |
+
voice_ctx.VOICES_DIR = self._old
|
| 118 |
+
(voice_ctx.REPORTS_JSONL, voice_ctx.REPORTS_OUTBOX, voice_ctx.OPERATIONS_LOG,
|
| 119 |
+
voice_ctx.HF_TOKEN_PATH) = self._old_reports
|
| 120 |
|
| 121 |
|
| 122 |
def ns(**kw):
|
tests/test_units.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
"""Offline unit tests: pure logic, no network. Must stay fast (<60s total)."""
|
| 2 |
import json
|
|
|
|
| 3 |
import struct
|
| 4 |
import sys
|
| 5 |
import tempfile
|
|
@@ -8,6 +9,7 @@ from pathlib import Path
|
|
| 8 |
|
| 9 |
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 10 |
from _helpers import IsolatedVoices, ns, voice # noqa: E402
|
|
|
|
| 11 |
|
| 12 |
import numpy as np
|
| 13 |
|
|
@@ -55,6 +57,25 @@ class TestNames(unittest.TestCase):
|
|
| 55 |
with self.assertRaises(SystemExit):
|
| 56 |
voice._resolve_name(ns(name="bad name!"), "fallback")
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
def test_registry_folder_lowercases(self):
|
| 59 |
self.assertTrue(str(voice._registry_folder("StyleTune-V2")).endswith("styletune-v2"))
|
| 60 |
|
|
@@ -271,7 +292,163 @@ class TestDeltaGates(unittest.TestCase):
|
|
| 271 |
v, b = self._pair(d)
|
| 272 |
out = voice._compute_delta(str(v), str(b), d / "out", "delta-x", ns())
|
| 273 |
got = read_arr(out, "output.weight")
|
| 274 |
-
self.assertTrue((got == 1.0).all())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
def test_niche_gates_fail_fast_non_interactive(self):
|
| 277 |
for mv, mb in ((False, True), (True, False), (True, True)):
|
|
@@ -353,6 +530,160 @@ class TestQuantShapes(unittest.TestCase):
|
|
| 353 |
|
| 354 |
|
| 355 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
class TestRunGuarded(unittest.TestCase):
|
| 357 |
def test_timeout_reaps_child(self):
|
| 358 |
import subprocess
|
|
@@ -382,5 +713,380 @@ class TestRunGuarded(unittest.TestCase):
|
|
| 382 |
r = run_guarded(["echo", "hi"], timeout=30)
|
| 383 |
self.assertEqual((r.returncode, r.stdout.strip()), (0, "hi"))
|
| 384 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
if __name__ == "__main__":
|
| 386 |
unittest.main()
|
|
|
|
| 1 |
"""Offline unit tests: pure logic, no network. Must stay fast (<60s total)."""
|
| 2 |
import json
|
| 3 |
+
import os
|
| 4 |
import struct
|
| 5 |
import sys
|
| 6 |
import tempfile
|
|
|
|
| 9 |
|
| 10 |
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 11 |
from _helpers import IsolatedVoices, ns, voice # noqa: E402
|
| 12 |
+
from vlib.pack import _resolve_pack_tensors # noqa: E402
|
| 13 |
|
| 14 |
import numpy as np
|
| 15 |
|
|
|
|
| 57 |
with self.assertRaises(SystemExit):
|
| 58 |
voice._resolve_name(ns(name="bad name!"), "fallback")
|
| 59 |
|
| 60 |
+
def test_public_source_passthrough(self):
|
| 61 |
+
for good in ("org/model", "google/gemma-4-12b", "model.gguf",
|
| 62 |
+
"voice-name", "", "β"):
|
| 63 |
+
self.assertEqual(voice._public_source(good), good, good)
|
| 64 |
+
|
| 65 |
+
def test_public_source_strips_local_paths(self):
|
| 66 |
+
self.assertEqual(
|
| 67 |
+
voice._public_source("/Users/someone/Downloads/m.gguf"), "m.gguf")
|
| 68 |
+
self.assertEqual(voice._public_source("./rel/m.gguf"), "m.gguf")
|
| 69 |
+
self.assertEqual(voice._public_source("~/m.gguf"), "m.gguf")
|
| 70 |
+
|
| 71 |
+
def test_public_source_strips_url_secrets(self):
|
| 72 |
+
self.assertEqual(
|
| 73 |
+
voice._public_source("https://user:pw@huggingface.co/o/m"),
|
| 74 |
+
"https://huggingface.co/o/m")
|
| 75 |
+
self.assertEqual(
|
| 76 |
+
voice._public_source("https://cdn.example/x.bin?sig=SECRET&t=1"),
|
| 77 |
+
"https://cdn.example/x.bin")
|
| 78 |
+
|
| 79 |
def test_registry_folder_lowercases(self):
|
| 80 |
self.assertTrue(str(voice._registry_folder("StyleTune-V2")).endswith("styletune-v2"))
|
| 81 |
|
|
|
|
| 292 |
v, b = self._pair(d)
|
| 293 |
out = voice._compute_delta(str(v), str(b), d / "out", "delta-x", ns())
|
| 294 |
got = read_arr(out, "output.weight")
|
| 295 |
+
self.assertTrue((got == 1.0).all(), "delta must equal voice(2.0) - base(1.0)")
|
| 296 |
+
|
| 297 |
+
def test_tied_base_head_falls_back_to_embed(self):
|
| 298 |
+
import io
|
| 299 |
+
from contextlib import redirect_stdout
|
| 300 |
+
d = Path(tempfile.mkdtemp())
|
| 301 |
+
v, b = d / "v.safetensors", d / "b.safetensors"
|
| 302 |
+
write_st(v, {"lm_head.weight": (np.full((4, 4), 2.0, dtype=np.float16), "F16"),
|
| 303 |
+
"model.language_model.embed_tokens.weight": (np.full((4, 4), 7.0, dtype=np.float16), "F16")})
|
| 304 |
+
write_st(b, {"model.language_model.embed_tokens.weight": (np.full((4, 4), 1.0, dtype=np.float16), "F16")})
|
| 305 |
+
buf = io.StringIO()
|
| 306 |
+
with redirect_stdout(buf):
|
| 307 |
+
out = voice._compute_delta(str(v), str(b), d / "out", "delta-x", ns())
|
| 308 |
+
self.assertTrue((read_arr(out, "lm_head.weight") == 1.0).all(), "head delta must equal voice head(2.0) - tied base embed(1.0)")
|
| 309 |
+
self.assertTrue((read_arr(out, "model.language_model.embed_tokens.weight") == 6.0).all(), "embed delta must equal voice embed(7.0) - base embed(1.0)")
|
| 310 |
+
self.assertIn("tied", buf.getvalue(), "fallback must be loud, not silent")
|
| 311 |
+
|
| 312 |
+
def test_tied_base_shape_mismatch_fails(self):
|
| 313 |
+
d = Path(tempfile.mkdtemp())
|
| 314 |
+
v, b = d / "v.safetensors", d / "b.safetensors"
|
| 315 |
+
write_st(v, {"lm_head.weight": (np.full((4, 4), 2.0, dtype=np.float16), "F16")})
|
| 316 |
+
write_st(b, {"model.language_model.embed_tokens.weight": (np.full((4, 8), 1.0, dtype=np.float16), "F16")})
|
| 317 |
+
with self.assertRaises(SystemExit):
|
| 318 |
+
voice._compute_delta(str(v), str(b), d / "out", "delta-x", ns())
|
| 319 |
+
|
| 320 |
+
def test_headless_baseless_still_fails(self):
|
| 321 |
+
d = Path(tempfile.mkdtemp())
|
| 322 |
+
v, b = d / "v.safetensors", d / "b.safetensors"
|
| 323 |
+
write_st(v, {"lm_head.weight": (np.full((4, 4), 2.0, dtype=np.float16), "F16")})
|
| 324 |
+
write_st(b, {"blk.0.attn.weight": (np.full((4, 4), 1.0, dtype=np.float16), "F16")})
|
| 325 |
+
with self.assertRaises(SystemExit):
|
| 326 |
+
voice._compute_delta(str(v), str(b), d / "out", "delta-x", ns())
|
| 327 |
+
|
| 328 |
+
def test_tied_head_want_reroutes_to_embed(self):
|
| 329 |
+
d = Path(tempfile.mkdtemp())
|
| 330 |
+
b = d / "tied.safetensors"
|
| 331 |
+
write_st(b, {"model.language_model.embed_tokens.weight": (np.full((4, 4), 1.0, dtype=np.float16), "F16")})
|
| 332 |
+
self.assertEqual(voice._tied_head_want(str(b), None, ["lm_head.weight"]),
|
| 333 |
+
["model.language_model.embed_tokens.weight"])
|
| 334 |
+
|
| 335 |
+
def test_tied_head_want_dedupes_embed(self):
|
| 336 |
+
d = Path(tempfile.mkdtemp())
|
| 337 |
+
b = d / "tied.safetensors"
|
| 338 |
+
write_st(b, {"model.language_model.embed_tokens.weight": (np.full((4, 4), 1.0, dtype=np.float16), "F16")})
|
| 339 |
+
self.assertEqual(voice._tied_head_want(str(b), None,
|
| 340 |
+
["lm_head.weight", "model.language_model.embed_tokens.weight"]),
|
| 341 |
+
["model.language_model.embed_tokens.weight"])
|
| 342 |
+
|
| 343 |
+
def test_tied_head_want_untied_unchanged(self):
|
| 344 |
+
d = Path(tempfile.mkdtemp())
|
| 345 |
+
b = d / "untied.safetensors"
|
| 346 |
+
write_st(b, {"lm_head.weight": (np.full((4, 4), 1.0, dtype=np.float16), "F16"),
|
| 347 |
+
"model.language_model.embed_tokens.weight": (np.full((4, 4), 1.0, dtype=np.float16), "F16")})
|
| 348 |
+
self.assertEqual(voice._tied_head_want(str(b), None, ["lm_head.weight"]), ["lm_head.weight"])
|
| 349 |
+
|
| 350 |
+
def test_tied_head_want_headless_unchanged(self):
|
| 351 |
+
d = Path(tempfile.mkdtemp())
|
| 352 |
+
b = d / "bare.safetensors"
|
| 353 |
+
write_st(b, {"blk.0.attn.weight": (np.full((4, 4), 1.0, dtype=np.float16), "F16")})
|
| 354 |
+
self.assertEqual(voice._tied_head_want(str(b), None, ["lm_head.weight"]), ["lm_head.weight"])
|
| 355 |
+
|
| 356 |
+
def test_gguf_delta_chunked_matches_full(self):
|
| 357 |
+
# Slow path (either side GGUF) must stream dequant in bounded chunks:
|
| 358 |
+
# patch the seam small + unaligned, spy max slice, assert values equal
|
| 359 |
+
# full-dequant math and every slice bounded by the seam.
|
| 360 |
+
from gguf import GGUFWriter
|
| 361 |
+
from gguf.constants import GGMLQuantizationType as Q
|
| 362 |
+
from gguf.quants import quantize
|
| 363 |
+
d = Path(tempfile.mkdtemp())
|
| 364 |
+
shape = (64, 128)
|
| 365 |
+
rng = np.random.default_rng(7)
|
| 366 |
+
vf = (rng.normal(size=shape) * 2).astype(np.float32)
|
| 367 |
+
bf = (rng.normal(size=shape) * 2).astype(np.float32)
|
| 368 |
+
vp, bp = d / "v.gguf", d / "b.gguf"
|
| 369 |
+
for p, f in ((vp, vf), (bp, bf)):
|
| 370 |
+
w = GGUFWriter(str(p), "llama", use_temp_file=False)
|
| 371 |
+
w.add_tensor("output.weight", quantize(f, Q.Q8_0), raw_dtype=Q.Q8_0)
|
| 372 |
+
w.write_header_to_file()
|
| 373 |
+
w.write_kv_data_to_file()
|
| 374 |
+
w.write_tensors_to_file()
|
| 375 |
+
w.close()
|
| 376 |
+
old_chunk = voice._DELTA_CHUNK_ELEMS
|
| 377 |
+
voice._DELTA_CHUNK_ELEMS = 100 # small and unaligned vs Q8_0 block 32
|
| 378 |
+
max_slice = []
|
| 379 |
+
orig_slice = voice._gguf_raw_slice_to_f32
|
| 380 |
+
def _spy(raw, qt, off, n):
|
| 381 |
+
max_slice.append(n)
|
| 382 |
+
return orig_slice(raw, qt, off, n)
|
| 383 |
+
voice._gguf_raw_slice_to_f32 = _spy
|
| 384 |
+
try:
|
| 385 |
+
out = voice._compute_delta(str(vp), str(bp), d / "out", "delta-x", ns())
|
| 386 |
+
finally:
|
| 387 |
+
voice._DELTA_CHUNK_ELEMS = old_chunk
|
| 388 |
+
voice._gguf_raw_slice_to_f32 = orig_slice
|
| 389 |
+
self.assertGreater(len(max_slice), 2, "expected many bounded chunks")
|
| 390 |
+
self.assertLessEqual(max(max_slice), 100, max_slice[:5])
|
| 391 |
+
vsrc, bsrc = voice.open_source(str(vp)), voice.open_source(str(bp))
|
| 392 |
+
want = (vsrc.read_f32("output.weight").astype(np.float32)
|
| 393 |
+
- bsrc.read_f32("output.weight").astype(np.float32)).astype("float16")
|
| 394 |
+
got = read_arr(out, "output.weight")
|
| 395 |
+
self.assertTrue((got == want).all())
|
| 396 |
+
def test_share_safe_delta_header(self):
|
| 397 |
+
import json as _j
|
| 398 |
+
d = Path(tempfile.mkdtemp())
|
| 399 |
+
home = d / "Users" / "someone" / "Desktop"
|
| 400 |
+
home.mkdir(parents=True)
|
| 401 |
+
self._pair(home)
|
| 402 |
+
out = voice._compute_delta(str(home / "v.safetensors"),
|
| 403 |
+
str(home / "b.safetensors"),
|
| 404 |
+
d / "out", "delta-x", ns())
|
| 405 |
+
outp = Path(str(out))
|
| 406 |
+
data = (outp / "voice.safetensors").read_bytes() \
|
| 407 |
+
if outp.is_dir() else outp.read_bytes()
|
| 408 |
+
hs = struct.unpack("<Q", data[:8])[0]
|
| 409 |
+
meta = _j.loads(data[8:8 + hs]).get("__metadata__", {})
|
| 410 |
+
blob = _j.dumps(meta)
|
| 411 |
+
self.assertNotIn("Users", blob)
|
| 412 |
+
self.assertNotIn("someone", blob)
|
| 413 |
+
self.assertEqual(meta.get("base"), "b.safetensors")
|
| 414 |
+
self.assertEqual(meta.get("voice"), "v.safetensors")
|
| 415 |
+
|
| 416 |
+
def test_verify_agrees(self):
|
| 417 |
+
d = Path(tempfile.mkdtemp())
|
| 418 |
+
write_st(d / "v.safetensors",
|
| 419 |
+
{"output.weight": (np.zeros((4, 4), dtype=np.float16), "F16")})
|
| 420 |
+
meta = {"tensor_name": "output.weight", "shape": [4, 4],
|
| 421 |
+
"dtype": "F16", "type": "voice"}
|
| 422 |
+
self.assertEqual(voice._verify_voice_file(str(d / "v.safetensors"), meta), [])
|
| 423 |
+
|
| 424 |
+
def test_verify_flags_mismatch(self):
|
| 425 |
+
d = Path(tempfile.mkdtemp())
|
| 426 |
+
write_st(d / "v.safetensors",
|
| 427 |
+
{"output.weight": (np.zeros((4, 4), dtype=np.float16), "F16")})
|
| 428 |
+
meta = {"tensor_name": "output.weight", "shape": [262144, 2048],
|
| 429 |
+
"dtype": "BF16", "type": "voice"}
|
| 430 |
+
warns = voice._verify_voice_file(str(d / "v.safetensors"), meta)
|
| 431 |
+
blob = "\n".join(warns)
|
| 432 |
+
self.assertIn("MISMATCH shape", blob)
|
| 433 |
+
self.assertIn("MISMATCH dtype", blob)
|
| 434 |
+
self.assertIn("file wins", blob)
|
| 435 |
+
|
| 436 |
+
def test_gguf_slice_q4k_superblocks(self):
|
| 437 |
+
# K-quants share scales across 256-elem superblocks: slices crossing
|
| 438 |
+
# superblock boundaries must equal full dequantize cut to the same range.
|
| 439 |
+
# (gguf lib can't quantize Q4_K here, so synthesize raw superblock bytes.)
|
| 440 |
+
from gguf.constants import GGMLQuantizationType as Q
|
| 441 |
+
from gguf.quants import dequantize
|
| 442 |
+
import numpy as _np
|
| 443 |
+
elems, bs, ts = 1024, 256, 144
|
| 444 |
+
raw = _np.random.default_rng(3).integers(0, 256, size=elems // bs * ts).astype(_np.uint8).tobytes()
|
| 445 |
+
full = dequantize(_np.frombuffer(raw, dtype=_np.uint8).reshape(-1, ts), Q.Q4_K).reshape(-1)
|
| 446 |
+
for off, n in ((0, 100), (200, 100), (100, 500), (900, 124), (0, 1024)):
|
| 447 |
+
b0 = off // bs
|
| 448 |
+
b1 = (off + n + bs - 1) // bs
|
| 449 |
+
seg = raw[b0 * ts:b1 * ts]
|
| 450 |
+
got = voice._gguf_raw_slice_to_f32(seg, Q.Q4_K, off - b0 * bs, n)
|
| 451 |
+
self.assertTrue((got == full[off:off + n]).all(), f"off={off} n={n}")
|
| 452 |
|
| 453 |
def test_niche_gates_fail_fast_non_interactive(self):
|
| 454 |
for mv, mb in ((False, True), (True, False), (True, True)):
|
|
|
|
| 530 |
|
| 531 |
|
| 532 |
|
| 533 |
+
class TestVoicePacks(IsolatedVoices):
|
| 534 |
+
def _pack_pair(self, d):
|
| 535 |
+
write_st(d / "v.safetensors", {
|
| 536 |
+
"output.weight": (np.full((4, 4), 3.0, np.float16), "F16"),
|
| 537 |
+
"model.norm.weight": (np.full((8,), 5.0, np.float16), "F16")})
|
| 538 |
+
write_st(d / "b.safetensors", {
|
| 539 |
+
"output.weight": (np.full((4, 4), 2.0, np.float16), "F16"),
|
| 540 |
+
"model.norm.weight": (np.full((8,), 4.0, np.float16), "F16"),
|
| 541 |
+
"extra.weight": (np.zeros((2, 2), np.float16), "F16")})
|
| 542 |
+
return d / "v.safetensors", d / "b.safetensors"
|
| 543 |
+
|
| 544 |
+
def test_pack_delta_computes_all(self):
|
| 545 |
+
d = Path(tempfile.mkdtemp())
|
| 546 |
+
v, b = self._pack_pair(d)
|
| 547 |
+
out = voice._compute_delta(str(v), str(b), d / "out", "delta-x", ns())
|
| 548 |
+
for name, want in (("output.weight", 1.0), ("model.norm.weight", 1.0)):
|
| 549 |
+
got = read_arr(out, name)
|
| 550 |
+
self.assertTrue((got == want).all(), name)
|
| 551 |
+
|
| 552 |
+
def test_pack_delta_missing_name_fails(self):
|
| 553 |
+
d = Path(tempfile.mkdtemp())
|
| 554 |
+
write_st(d / "v.safetensors",
|
| 555 |
+
{"ghost.weight": (np.zeros((2, 2), np.float16), "F16")})
|
| 556 |
+
write_st(d / "b.safetensors",
|
| 557 |
+
{"other.weight": (np.zeros((2, 2), np.float16), "F16")})
|
| 558 |
+
with self.assertRaises(SystemExit):
|
| 559 |
+
voice._compute_delta(str(d / "v.safetensors"), str(d / "b.safetensors"),
|
| 560 |
+
d / "out", "delta-x", ns())
|
| 561 |
+
|
| 562 |
+
def test_tensor_cast_delta_fuses_not_replaces(self):
|
| 563 |
+
from gguf import GGUFReader, GGUFWriter
|
| 564 |
+
from gguf.constants import GGMLQuantizationType as Q
|
| 565 |
+
d = Path(tempfile.mkdtemp())
|
| 566 |
+
write_st(d / "v.safetensors",
|
| 567 |
+
{"output.weight": (np.full((4, 4), 3.0, np.float16), "F16")})
|
| 568 |
+
write_st(d / "b.safetensors",
|
| 569 |
+
{"output.weight": (np.full((4, 4), 2.0, np.float16), "F16")})
|
| 570 |
+
dout = voice._compute_delta(str(d / "v.safetensors"), str(d / "b.safetensors"),
|
| 571 |
+
d / "dout", "delta-x", ns())
|
| 572 |
+
tp = d / "t.gguf"
|
| 573 |
+
w = GGUFWriter(str(tp), "llama", use_temp_file=False)
|
| 574 |
+
w.add_architecture()
|
| 575 |
+
w.add_tensor("output.weight", np.full((4, 4), 10.0, np.float16), raw_dtype=Q.F16)
|
| 576 |
+
w.write_header_to_file()
|
| 577 |
+
w.write_kv_data_to_file()
|
| 578 |
+
w.write_tensors_to_file()
|
| 579 |
+
w.close()
|
| 580 |
+
out = d / "voiced.gguf"
|
| 581 |
+
voice.cmd_cast(ns(target=str(tp), voice=str(dout), out=str(out)))
|
| 582 |
+
got = [t for t in GGUFReader(str(out)).tensors if t.name == "output.weight"][0]
|
| 583 |
+
# base 10 + delta (3-2) = 11. Raw-delta replace would give 1.
|
| 584 |
+
self.assertTrue((np.asarray(got.data, dtype=np.float32) == 11.0).all())
|
| 585 |
+
|
| 586 |
+
def test_pack_cast_gguf_fuses_all(self):
|
| 587 |
+
from gguf import GGUFReader, GGUFWriter
|
| 588 |
+
from gguf.constants import GGMLQuantizationType as Q
|
| 589 |
+
d = Path(tempfile.mkdtemp())
|
| 590 |
+
v, b = self._pack_pair(d)
|
| 591 |
+
dout = voice._compute_delta(str(v), str(b), d / "dout", "delta-x", ns())
|
| 592 |
+
tp = d / "t.gguf"
|
| 593 |
+
w = GGUFWriter(str(tp), "llama", use_temp_file=False)
|
| 594 |
+
w.add_architecture()
|
| 595 |
+
w.add_tensor("output.weight", np.full((4, 4), 10.0, np.float16), raw_dtype=Q.F16)
|
| 596 |
+
w.add_tensor("model.norm.weight", np.full((8,), 20.0, np.float32), raw_dtype=Q.F32)
|
| 597 |
+
w.write_header_to_file()
|
| 598 |
+
w.write_kv_data_to_file()
|
| 599 |
+
w.write_tensors_to_file()
|
| 600 |
+
w.close()
|
| 601 |
+
out = d / "voiced.gguf"
|
| 602 |
+
voice.cmd_cast(ns(target=str(tp), voice=str(dout), out=str(out)))
|
| 603 |
+
back = {t.name: t for t in GGUFReader(str(out)).tensors}
|
| 604 |
+
self.assertTrue((np.asarray(back["output.weight"].data, dtype=np.float32) == 11.0).all())
|
| 605 |
+
self.assertTrue((np.asarray(back["model.norm.weight"].data, dtype=np.float32) == 21.0).all())
|
| 606 |
+
|
| 607 |
+
def test_pack_cast_safetensors_fuses_all(self):
|
| 608 |
+
|
| 609 |
+
d = Path(tempfile.mkdtemp())
|
| 610 |
+
v, b = self._pack_pair(d)
|
| 611 |
+
dout = voice._compute_delta(str(v), str(b), d / "dout", "delta-x", ns())
|
| 612 |
+
write_st(d / "t.safetensors", {
|
| 613 |
+
"output.weight": (np.full((4, 4), 10.0, np.float16), "F16"),
|
| 614 |
+
"model.norm.weight": (np.full((8,), 20.0, np.float16), "F16")})
|
| 615 |
+
out = d / "o.safetensors"
|
| 616 |
+
voice.cmd_cast(ns(target=str(d / "t.safetensors"),
|
| 617 |
+
voice=str(dout), out=str(out)))
|
| 618 |
+
for name, want in (("output.weight", 11.0), ("model.norm.weight", 21.0)):
|
| 619 |
+
got = read_arr(out, name)
|
| 620 |
+
self.assertTrue((got == want).all(), name)
|
| 621 |
+
|
| 622 |
+
def test_pack_cast_safetensors_single_pass(self):
|
| 623 |
+
# The old chain called rewrite_safetensors_tensor once per tensor
|
| 624 |
+
# (O(N.file) full copies). The fused path must succeed with it disabled.
|
| 625 |
+
d = Path(tempfile.mkdtemp())
|
| 626 |
+
v, b = self._pack_pair(d)
|
| 627 |
+
dout = voice._compute_delta(str(v), str(b), d / "dout", "delta-x", ns())
|
| 628 |
+
write_st(d / "t.safetensors", {
|
| 629 |
+
"output.weight": (np.full((4, 4), 10.0, np.float16), "F16"),
|
| 630 |
+
"model.norm.weight": (np.full((8,), 20.0, np.float16), "F16")})
|
| 631 |
+
out = d / "o.safetensors"
|
| 632 |
+
orig = voice.rewrite_safetensors_tensor
|
| 633 |
+
def _boom(*a, **k):
|
| 634 |
+
raise AssertionError("chained per-tensor rewrite used")
|
| 635 |
+
voice.rewrite_safetensors_tensor = _boom
|
| 636 |
+
try:
|
| 637 |
+
voice.cmd_cast(ns(target=str(d / "t.safetensors"),
|
| 638 |
+
voice=str(dout), out=str(out)))
|
| 639 |
+
finally:
|
| 640 |
+
voice.rewrite_safetensors_tensor = orig
|
| 641 |
+
for name, want in (("output.weight", 11.0), ("model.norm.weight", 21.0)):
|
| 642 |
+
got = read_arr(out, name)
|
| 643 |
+
self.assertTrue((got == want).all(), name)
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
class TestBaseCache(unittest.TestCase):
|
| 647 |
+
def _cache(self, d, tensors):
|
| 648 |
+
write_st(d / "base.safetensors", tensors)
|
| 649 |
+
return d / "base.safetensors"
|
| 650 |
+
|
| 651 |
+
def test_validate_hit(self):
|
| 652 |
+
d = Path(tempfile.mkdtemp())
|
| 653 |
+
p = self._cache(d, {"a": (np.zeros((4, 4), np.float16), "F16")})
|
| 654 |
+
self.assertEqual(voice._cache_validate(str(p), ["a"]), [])
|
| 655 |
+
|
| 656 |
+
def test_validate_missing_and_truncated(self):
|
| 657 |
+
import struct as _st, json as _j
|
| 658 |
+
d = Path(tempfile.mkdtemp())
|
| 659 |
+
p = self._cache(d, {"a": (np.zeros((4, 4), np.float16), "F16")})
|
| 660 |
+
raw = bytearray(p.read_bytes())
|
| 661 |
+
hs = _st.unpack("<Q", bytes(raw[:8]))[0]
|
| 662 |
+
hdr = _j.loads(bytes(raw[8:8 + hs]))
|
| 663 |
+
hdr["b"] = {"dtype": "F16", "shape": [8], "data_offsets": [32, 48]}
|
| 664 |
+
hj = _j.dumps(hdr).encode()
|
| 665 |
+
p.write_bytes(_st.pack("<Q", len(hj)) + hj + bytes(raw[8 + hs:]))
|
| 666 |
+
self.assertEqual(sorted(voice._cache_validate(str(p), ["a", "b", "c"])), ["b", "c"])
|
| 667 |
+
|
| 668 |
+
def test_validate_garbage(self):
|
| 669 |
+
d = Path(tempfile.mkdtemp())
|
| 670 |
+
p = d / "base.safetensors"
|
| 671 |
+
p.write_bytes(b"not-a-safetensors-file")
|
| 672 |
+
self.assertEqual(voice._cache_validate(str(p), ["a"]), ["a"])
|
| 673 |
+
|
| 674 |
+
def test_topup_write_roundtrip(self):
|
| 675 |
+
d = Path(tempfile.mkdtemp())
|
| 676 |
+
p = self._cache(d, {"a": (np.full((4, 4), 7.0, np.float16), "F16")})
|
| 677 |
+
rawf = d / "raw.bin"
|
| 678 |
+
rawf.write_bytes(np.full((8,), 9.0, np.float16).tobytes())
|
| 679 |
+
staged = [("b", np.full((2, 2), 3.0, np.float16).tobytes(), "F16", (2, 2), None),
|
| 680 |
+
("c", None, "F16", (8,), rawf)]
|
| 681 |
+
new_hdr = voice._cache_topup_write(p, staged)
|
| 682 |
+
self.assertEqual(sorted(k for k in new_hdr if k != "__metadata__"), ["a", "b", "c"])
|
| 683 |
+
for name, want in (("a", 7.0), ("b", 3.0), ("c", 9.0)):
|
| 684 |
+
self.assertTrue((read_arr(p, name) == want).all(), name)
|
| 685 |
+
|
| 686 |
+
|
| 687 |
class TestRunGuarded(unittest.TestCase):
|
| 688 |
def test_timeout_reaps_child(self):
|
| 689 |
import subprocess
|
|
|
|
| 713 |
r = run_guarded(["echo", "hi"], timeout=30)
|
| 714 |
self.assertEqual((r.returncode, r.stdout.strip()), (0, "hi"))
|
| 715 |
|
| 716 |
+
class TestAliases(unittest.TestCase):
|
| 717 |
+
GGUF_NAMES = ["token_embd.weight", "blk.28.attn_o.weight", "blk.29.attn_o.weight",
|
| 718 |
+
"blk.29.ffn_down.weight", "output_norm.weight", "output.weight"]
|
| 719 |
+
|
| 720 |
+
def test_alias_both_directions(self):
|
| 721 |
+
self.assertEqual(voice._pack_aliases("model.language_model.embed_tokens.weight", self.GGUF_NAMES),
|
| 722 |
+
["token_embd.weight"])
|
| 723 |
+
self.assertEqual(voice._pack_aliases("token_embd.weight",
|
| 724 |
+
["model.language_model.embed_tokens.weight", "output.weight"]),
|
| 725 |
+
["model.language_model.embed_tokens.weight"])
|
| 726 |
+
self.assertEqual(voice._pack_aliases("model.language_model.layers.29.self_attn.o_proj.weight",
|
| 727 |
+
self.GGUF_NAMES),
|
| 728 |
+
["blk.29.attn_o.weight"])
|
| 729 |
+
self.assertEqual(voice._pack_aliases("lm_head.weight", self.GGUF_NAMES), ["output.weight"])
|
| 730 |
+
|
| 731 |
+
def test_alias_block_mismatch_is_no_match(self):
|
| 732 |
+
self.assertEqual(voice._pack_aliases("model.language_model.layers.28.self_attn.o_proj.weight",
|
| 733 |
+
["blk.29.attn_o.weight"]), [])
|
| 734 |
+
self.assertEqual(voice._pack_aliases("someday.weight", self.GGUF_NAMES), [])
|
| 735 |
+
|
| 736 |
+
def test_alias_ambiguous_lists_all(self):
|
| 737 |
+
self.assertEqual(sorted(voice._pack_aliases("model.embed_tokens.weight",
|
| 738 |
+
["a.token_embd.weight", "b.token_embd.weight"])),
|
| 739 |
+
["a.token_embd.weight", "b.token_embd.weight"])
|
| 740 |
+
|
| 741 |
+
def test_alias_modern_gguf_variants(self):
|
| 742 |
+
# Evidenced live: Gemma3/Qwen3 GGUFs use attn_output + post_attention_norm.
|
| 743 |
+
names = ["blk.29.attn_output.weight", "blk.29.post_attention_norm.weight"]
|
| 744 |
+
self.assertEqual(voice._pack_aliases("model.layers.29.self_attn.o_proj.weight", names),
|
| 745 |
+
["blk.29.attn_output.weight"])
|
| 746 |
+
self.assertEqual(voice._pack_aliases("model.layers.29.post_attention_layernorm.weight", names),
|
| 747 |
+
["blk.29.post_attention_norm.weight"])
|
| 748 |
+
|
| 749 |
+
def test_alias_glm_generations(self):
|
| 750 |
+
# Evidenced live: ChatGLM-style HF names vs llama.cpp GGUF names.
|
| 751 |
+
gguf = ["token_embd.weight", "blk.39.attn_output.weight",
|
| 752 |
+
"blk.39.ffn_down.weight", "output_norm.weight", "output.weight"]
|
| 753 |
+
self.assertEqual(voice._pack_aliases("transformer.encoder.layers.39.self_attention.dense.weight", gguf),
|
| 754 |
+
["blk.39.attn_output.weight"])
|
| 755 |
+
self.assertEqual(voice._pack_aliases("transformer.encoder.layers.39.mlp.dense_4h_to_h.weight", gguf),
|
| 756 |
+
["blk.39.ffn_down.weight"])
|
| 757 |
+
self.assertEqual(voice._pack_aliases("transformer.encoder.final_layernorm.weight", gguf),
|
| 758 |
+
["output_norm.weight"])
|
| 759 |
+
self.assertEqual(voice._pack_aliases("transformer.output_layer.weight", gguf),
|
| 760 |
+
["output.weight"])
|
| 761 |
+
|
| 762 |
+
def test_pack_preset_head_only(self):
|
| 763 |
+
# Preset is the output head, nothing else. Tied repos resolve the embed
|
| 764 |
+
# (it doubles as head); untied resolve lm_head.
|
| 765 |
+
d = Path(tempfile.mkdtemp())
|
| 766 |
+
write_st(d / "tied.safetensors", {
|
| 767 |
+
"model.language_model.embed_tokens.weight": (np.zeros((8, 4), np.float16), "F16"),
|
| 768 |
+
"model.language_model.norm.weight": (np.zeros((4,), np.float16), "F16")})
|
| 769 |
+
write_st(d / "untied.safetensors", {
|
| 770 |
+
"lm_head.weight": (np.zeros((8, 4), np.float16), "F16"),
|
| 771 |
+
"model.embed_tokens.weight": (np.zeros((8, 4), np.float16), "F16"),
|
| 772 |
+
"model.norm.weight": (np.zeros((4,), np.float16), "F16")})
|
| 773 |
+
src = voice.open_source(str(d / "tied.safetensors"))
|
| 774 |
+
self.assertEqual(_resolve_pack_tensors(src, "r", None, src.names()),
|
| 775 |
+
["model.language_model.embed_tokens.weight"])
|
| 776 |
+
src = voice.open_source(str(d / "untied.safetensors"))
|
| 777 |
+
self.assertEqual(_resolve_pack_tensors(src, "r", None, src.names()),
|
| 778 |
+
["lm_head.weight"])
|
| 779 |
+
|
| 780 |
+
def test_alias_moe_experts(self):
|
| 781 |
+
# Evidenced live: Qwen3-30B-A3B GGUF experts vs HF experts.* names.
|
| 782 |
+
gguf = ["blk.5.ffn_down_exps.weight", "blk.5.ffn_gate_inp.weight"]
|
| 783 |
+
self.assertEqual(voice._pack_aliases("model.layers.5.experts.down_proj", gguf),
|
| 784 |
+
["blk.5.ffn_down_exps.weight"])
|
| 785 |
+
# fused HF gate_up has no 1:1 GGUF counterpart (split exps) β no match, fail loud downstream
|
| 786 |
+
self.assertEqual(voice._pack_aliases("model.layers.5.experts.gate_up_proj", gguf), [])
|
| 787 |
+
# routers never map (semantics differ per arch)
|
| 788 |
+
self.assertEqual(voice._pack_aliases("model.layers.5.router.per_expert_scale", gguf), [])
|
| 789 |
+
|
| 790 |
+
def test_pack_cast_bridges_naming(self):
|
| 791 |
+
# ST pack names onto a GGUF target speaking llama.cpp names (replace mode).
|
| 792 |
+
from gguf import GGUFReader, GGUFWriter
|
| 793 |
+
from gguf.constants import GGMLQuantizationType as Q
|
| 794 |
+
d = Path(tempfile.mkdtemp())
|
| 795 |
+
write_st(d / "pack.safetensors", {
|
| 796 |
+
"model.language_model.embed_tokens.weight": (np.full((4, 4), 3.0, np.float16), "F16"),
|
| 797 |
+
"lm_head.weight": (np.full((4, 4), 7.0, np.float16), "F16")})
|
| 798 |
+
tp = d / "t.gguf"
|
| 799 |
+
w = GGUFWriter(str(tp), "llama", use_temp_file=False)
|
| 800 |
+
w.add_architecture()
|
| 801 |
+
w.add_tensor("token_embd.weight", np.zeros((4, 4), dtype=np.float16), raw_dtype=Q.F16)
|
| 802 |
+
w.add_tensor("output.weight", np.zeros((4, 4), dtype=np.float16), raw_dtype=Q.F16)
|
| 803 |
+
w.write_header_to_file()
|
| 804 |
+
w.write_kv_data_to_file()
|
| 805 |
+
w.write_tensors_to_file()
|
| 806 |
+
w.close()
|
| 807 |
+
out = d / "voiced.gguf"
|
| 808 |
+
voice.cmd_cast(ns(target=str(tp), voice=str(d / "pack.safetensors"), out=str(out)))
|
| 809 |
+
back = {t.name: np.asarray(t.data, dtype=np.float32) for t in GGUFReader(str(out)).tensors}
|
| 810 |
+
self.assertTrue((back["token_embd.weight"] == 3.0).all())
|
| 811 |
+
self.assertTrue((back["output.weight"] == 7.0).all())
|
| 812 |
+
|
| 813 |
+
|
| 814 |
+
def test_nearest_encodable_mapping(self):
|
| 815 |
+
from gguf.constants import GGMLQuantizationType as Q
|
| 816 |
+
self.assertEqual(voice._nearest_encodable(Q.IQ4_NL), Q.Q4_0) # same 4.5bpp
|
| 817 |
+
self.assertEqual(voice._nearest_encodable(Q.Q4_K), Q.Q4_0)
|
| 818 |
+
self.assertEqual(voice._nearest_encodable(Q.Q6_K), Q.Q5_1)
|
| 819 |
+
|
| 820 |
+
def test_unencodable_target_keeps_nearest(self):
|
| 821 |
+
# Q4_K target (decode-only in gguf lib): fused output lands on Q4_0, not F16.
|
| 822 |
+
from gguf import GGUFReader, GGUFWriter
|
| 823 |
+
from gguf.constants import GGMLQuantizationType as Q
|
| 824 |
+
from gguf.quants import quant_shape_to_byte_shape
|
| 825 |
+
d = Path(tempfile.mkdtemp())
|
| 826 |
+
logical = (256, 256)
|
| 827 |
+
raw = np.zeros(quant_shape_to_byte_shape(logical, Q.Q4_K), dtype=np.uint8)
|
| 828 |
+
tp = d / "t.gguf"
|
| 829 |
+
w = GGUFWriter(str(tp), "llama", use_temp_file=False)
|
| 830 |
+
w.add_architecture()
|
| 831 |
+
w.add_tensor("blk.0.ffn_down.weight", raw, raw_dtype=Q.Q4_K)
|
| 832 |
+
w.write_header_to_file()
|
| 833 |
+
w.write_kv_data_to_file()
|
| 834 |
+
w.write_tensors_to_file()
|
| 835 |
+
w.close()
|
| 836 |
+
got = [t for t in GGUFReader(str(tp)).tensors if t.name == "blk.0.ffn_down.weight"][0]
|
| 837 |
+
self.assertEqual(got.tensor_type, Q.Q4_K)
|
| 838 |
+
write_st(d / "v.safetensors",
|
| 839 |
+
{"blk.0.ffn_down.weight": (np.full(logical, 1.0, np.float16), "F16")})
|
| 840 |
+
write_st(d / "b.safetensors",
|
| 841 |
+
{"blk.0.ffn_down.weight": (np.zeros(logical, np.float16), "F16")})
|
| 842 |
+
dout = voice._compute_delta(str(d / "v.safetensors"), str(d / "b.safetensors"),
|
| 843 |
+
d / "dout", "delta-x", ns())
|
| 844 |
+
out = d / "voiced.gguf"
|
| 845 |
+
voice.cmd_cast(ns(target=str(tp), voice=str(dout), out=str(out)))
|
| 846 |
+
fused = [t for t in GGUFReader(str(out)).tensors if t.name == "blk.0.ffn_down.weight"][0]
|
| 847 |
+
self.assertEqual(fused.tensor_type, Q.Q4_0)
|
| 848 |
+
|
| 849 |
+
|
| 850 |
+
def test_batch_miss_reports_all_with_hints(self):
|
| 851 |
+
# One failure names EVERY miss (no fix-forward) with same-size hints.
|
| 852 |
+
import io
|
| 853 |
+
from contextlib import redirect_stdout
|
| 854 |
+
names = ["token_embd.weight", "blk.29.attn_o.weight", "other.weight"]
|
| 855 |
+
counts = {"token_embd.weight": 16, "blk.29.attn_o.weight": 16, "other.weight": 64}
|
| 856 |
+
pairs = [("model.language_model.layers.29.self_attn.o_proj.weight", (4, 4)),
|
| 857 |
+
("ghost.weight", (8, 8)),
|
| 858 |
+
("phantom.weight", (2, 2))]
|
| 859 |
+
buf = io.StringIO()
|
| 860 |
+
with redirect_stdout(buf):
|
| 861 |
+
with self.assertRaises(SystemExit):
|
| 862 |
+
voice._resolve_pack_targets(pairs, names, "t.gguf", counts)
|
| 863 |
+
blob = buf.getvalue()
|
| 864 |
+
self.assertIn("ghost.weight", blob)
|
| 865 |
+
self.assertIn("phantom.weight", blob)
|
| 866 |
+
self.assertIn("other.weight", blob) # same-size hint for the 8x8 ghost
|
| 867 |
+
self.assertIn("2 tensor(s)", blob) # both misses at once; the aliased one is not listed
|
| 868 |
+
# all-resolvable returns the mapping (exact + alias)
|
| 869 |
+
got = voice._resolve_pack_targets(
|
| 870 |
+
[("model.language_model.layers.29.self_attn.o_proj.weight", (4, 4)),
|
| 871 |
+
("token_embd.weight", (4, 4))],
|
| 872 |
+
names, "t.gguf", counts)
|
| 873 |
+
self.assertEqual(got, {"model.language_model.layers.29.self_attn.o_proj.weight": "blk.29.attn_o.weight",
|
| 874 |
+
"token_embd.weight": "token_embd.weight"})
|
| 875 |
+
|
| 876 |
+
def test_pack_cast_bridges_same_format_prefix(self):
|
| 877 |
+
# Qwen-style prefixed voice onto bare names: same (block, trailing
|
| 878 |
+
# role) + equal count maps without growing _ROLE_ALIASES.
|
| 879 |
+
import io
|
| 880 |
+
from contextlib import redirect_stdout
|
| 881 |
+
got = voice._resolve_pack_targets(
|
| 882 |
+
[("model.language_model.layers.29.self_attn.o_proj.weight", (4, 4))],
|
| 883 |
+
["model.layers.29.self_attn.o_proj.weight"], "t.safetensors",
|
| 884 |
+
{"model.layers.29.self_attn.o_proj.weight": 16})
|
| 885 |
+
self.assertEqual(got, {"model.language_model.layers.29.self_attn.o_proj.weight":
|
| 886 |
+
"model.layers.29.self_attn.o_proj.weight"})
|
| 887 |
+
# β¦but a count mismatch stays loud, not a silent wrong-slot write.
|
| 888 |
+
buf = io.StringIO()
|
| 889 |
+
with redirect_stdout(buf):
|
| 890 |
+
with self.assertRaises(SystemExit):
|
| 891 |
+
voice._resolve_pack_targets(
|
| 892 |
+
[("model.language_model.layers.29.self_attn.o_proj.weight", (4, 8))],
|
| 893 |
+
["model.layers.29.self_attn.o_proj.weight"], "t.safetensors",
|
| 894 |
+
{"model.layers.29.self_attn.o_proj.weight": 16})
|
| 895 |
+
self.assertIn("not found", buf.getvalue())
|
| 896 |
+
|
| 897 |
+
class TestEntry(IsolatedVoices):
|
| 898 |
+
"""Launch-path proofs: the P3 split moved commands into vlib/, so entry
|
| 899 |
+
identity (__main__) must be resolved explicitly, never via __file__."""
|
| 900 |
+
|
| 901 |
+
def test_entry_script_resolves_main(self):
|
| 902 |
+
import types
|
| 903 |
+
fake = types.SimpleNamespace(__file__="voice.py")
|
| 904 |
+
old = sys.modules.get("__main__")
|
| 905 |
+
sys.modules["__main__"] = fake
|
| 906 |
+
try:
|
| 907 |
+
got = voice._entry_script()
|
| 908 |
+
finally:
|
| 909 |
+
if old is not None:
|
| 910 |
+
sys.modules["__main__"] = old
|
| 911 |
+
self.assertTrue(os.path.isabs(got), got)
|
| 912 |
+
self.assertTrue(got.endswith("voice.py"), got)
|
| 913 |
+
|
| 914 |
+
def test_entry_script_unknown_is_none(self):
|
| 915 |
+
import types
|
| 916 |
+
old = sys.modules.get("__main__")
|
| 917 |
+
sys.modules["__main__"] = types.SimpleNamespace()
|
| 918 |
+
try:
|
| 919 |
+
self.assertIsNone(voice._entry_script())
|
| 920 |
+
finally:
|
| 921 |
+
if old is not None:
|
| 922 |
+
sys.modules["__main__"] = old
|
| 923 |
+
|
| 924 |
+
def test_venv_reexec_targets_entry(self):
|
| 925 |
+
# Re-exec must run voice.py (absolute) with argv intact β never the module.
|
| 926 |
+
import types
|
| 927 |
+
d = Path(tempfile.mkdtemp())
|
| 928 |
+
vp = d / ".venv" / "bin"
|
| 929 |
+
vp.mkdir(parents=True)
|
| 930 |
+
(vp / "python").symlink_to(sys.executable)
|
| 931 |
+
from vlib import ctx
|
| 932 |
+
old_dir, old_main, old_argv = ctx.VOICES_DIR, sys.modules.get("__main__"), sys.argv[:]
|
| 933 |
+
old_venv = os.environ.pop("VOICE_NO_VENV", None)
|
| 934 |
+
ctx.VOICES_DIR = d
|
| 935 |
+
sys.modules["__main__"] = types.SimpleNamespace(__file__="voice.py")
|
| 936 |
+
sys.argv = ["voice.py", "delta", "x"]
|
| 937 |
+
captured = {}
|
| 938 |
+
orig_execv = os.execv
|
| 939 |
+
|
| 940 |
+
class _Stop(BaseException):
|
| 941 |
+
pass
|
| 942 |
+
|
| 943 |
+
os.execv = lambda p, a: captured.update(path=p, argv=a) or (_ for _ in ()).throw(_Stop())
|
| 944 |
+
try:
|
| 945 |
+
with self.assertRaises(_Stop):
|
| 946 |
+
voice._ensure_venv("delta")
|
| 947 |
+
finally:
|
| 948 |
+
os.execv = orig_execv
|
| 949 |
+
ctx.VOICES_DIR = old_dir
|
| 950 |
+
if old_main is not None:
|
| 951 |
+
sys.modules["__main__"] = old_main
|
| 952 |
+
sys.argv = old_argv
|
| 953 |
+
if old_venv is not None:
|
| 954 |
+
os.environ["VOICE_NO_VENV"] = old_venv
|
| 955 |
+
self.assertTrue(captured["argv"][1].endswith("/voice.py"), captured["argv"])
|
| 956 |
+
self.assertNotIn("cmds_misc", captured["argv"][1])
|
| 957 |
+
self.assertEqual(captured["argv"][2:], ["delta", "x"])
|
| 958 |
+
|
| 959 |
+
def test_entry_help_runs_main(self):
|
| 960 |
+
# The suite calls cmd_* directly and never executes main()'s parser
|
| 961 |
+
# wiring β a deleted flag or bad indent kills every real invocation
|
| 962 |
+
# while the suite stays green. This pins the entry path end to end.
|
| 963 |
+
import subprocess
|
| 964 |
+
root = Path(__file__).resolve().parent.parent
|
| 965 |
+
env = dict(os.environ, VOICE_NO_VENV="1")
|
| 966 |
+
outs = []
|
| 967 |
+
for argv in (["--help"], ["cast", "--help"], ["list"]):
|
| 968 |
+
r = subprocess.run([sys.executable, str(root / "voice.py"), *argv],
|
| 969 |
+
capture_output=True, text=True, timeout=120, env=env)
|
| 970 |
+
self.assertEqual(r.returncode, 0, (argv, r.stderr[-500:]))
|
| 971 |
+
outs.append(r.stdout)
|
| 972 |
+
self.assertIn("{get,cast,delta,add,list,info,remove,cleanup,auth,report,path,doctor}", outs[0])
|
| 973 |
+
self.assertIn("--speak", outs[1])
|
| 974 |
+
|
| 975 |
+
def test_voice_tensors_coerces_bare_strings(self):
|
| 976 |
+
# Hand-written voice.json may list tensors as bare strings; readers
|
| 977 |
+
# must render them, never traceback (P1 crash: only.get on a str).
|
| 978 |
+
got, synth = voice._voice_tensors({"tensors": ["lm_head.weight"]})
|
| 979 |
+
self.assertEqual(got, [{"name": "lm_head.weight"}])
|
| 980 |
+
self.assertFalse(synth)
|
| 981 |
+
|
| 982 |
+
def test_quant_override_byte_change_fails_fast(self):
|
| 983 |
+
# --quant f16 on an F32 safetensors target can never fit the fixed
|
| 984 |
+
# offsets: fail up front with guidance, not warn-then-dump (B1).
|
| 985 |
+
import io
|
| 986 |
+
from contextlib import redirect_stdout
|
| 987 |
+
d = Path(tempfile.mkdtemp())
|
| 988 |
+
write_st(d / "t.safetensors",
|
| 989 |
+
{"output.weight": (np.zeros((4, 4), np.float32), "F32")})
|
| 990 |
+
write_st(d / "v.safetensors",
|
| 991 |
+
{"output.weight": (np.zeros((4, 4), np.float32), "F32")})
|
| 992 |
+
header, tensors = voice.read_safetensors(str(d / "t.safetensors"))
|
| 993 |
+
src = voice.open_source(str(d / "v.safetensors"))
|
| 994 |
+
ref = src.ref("output.weight")
|
| 995 |
+
buf = io.StringIO()
|
| 996 |
+
with redirect_stdout(buf):
|
| 997 |
+
with self.assertRaises(SystemExit):
|
| 998 |
+
voice._cast_st_prepare(str(d / "t.safetensors"), tensors,
|
| 999 |
+
src.read_f32("output.weight"), ref, {},
|
| 1000 |
+
ns(quant="f16"))
|
| 1001 |
+
self.assertIn("different byte size", buf.getvalue())
|
| 1002 |
+
|
| 1003 |
+
def test_cancel_exits_nonzero(self):
|
| 1004 |
+
# Abandoned prompts (EOF/decline) must exit nonzero so scripts can
|
| 1005 |
+
# tell cancel from success (B2 contract).
|
| 1006 |
+
from unittest import mock
|
| 1007 |
+
from vlib.ui import _confirm
|
| 1008 |
+
with mock.patch("builtins.input", side_effect=EOFError), \
|
| 1009 |
+
mock.patch.object(sys.stdin, "isatty", return_value=True):
|
| 1010 |
+
with self.assertRaises(SystemExit) as cm:
|
| 1011 |
+
_confirm(" Overwrite? [y/N]: ",
|
| 1012 |
+
ns(non_interactive=False, yes=False))
|
| 1013 |
+
self.assertNotEqual(cm.exception.code, 0)
|
| 1014 |
+
|
| 1015 |
+
def test_requested_subset_routes_by_role(self):
|
| 1016 |
+
# An explicitly named tensor must land in its own slot, never the
|
| 1017 |
+
# output-first fallback (D2: embed request fitted onto lm_head).
|
| 1018 |
+
d = Path(tempfile.mkdtemp())
|
| 1019 |
+
write_st(d / "v.safetensors", {
|
| 1020 |
+
"model.embed_tokens.weight": (np.full((4, 3), 7.0, np.float16), "F16"),
|
| 1021 |
+
"model.lm_head.weight": (np.full((8, 4), 8.0, np.float16), "F16")})
|
| 1022 |
+
write_st(d / "t.safetensors", {
|
| 1023 |
+
"model.embed_tokens.weight": (np.full((4, 3), 1.0, np.float16), "F16"),
|
| 1024 |
+
"model.lm_head.weight": (np.full((8, 4), 2.0, np.float16), "F16")})
|
| 1025 |
+
out = d / "o.safetensors"
|
| 1026 |
+
voice.cmd_cast(ns(target=str(d / "t.safetensors"), voice=str(d / "v.safetensors"),
|
| 1027 |
+
out=str(out), tensors=["model.embed_tokens.weight"]))
|
| 1028 |
+
self.assertTrue((read_arr(out, "model.embed_tokens.weight", np.float16) == 7.0).all())
|
| 1029 |
+
self.assertTrue((read_arr(out, "model.lm_head.weight", np.float16) == 2.0).all())
|
| 1030 |
+
|
| 1031 |
+
|
| 1032 |
+
class TestSymlinkHardening(IsolatedVoices):
|
| 1033 |
+
"""Proof tests for the P3 symlink hardening (local CLI, self-owned dir)."""
|
| 1034 |
+
|
| 1035 |
+
def test_unresolvable_path_fails_closed(self):
|
| 1036 |
+
# Old code swallowed resolve errors and returned (fail-open).
|
| 1037 |
+
with self.assertRaises(SystemExit):
|
| 1038 |
+
voice._assert_not_symlink("/tmp/voices-probe\x00evil")
|
| 1039 |
+
|
| 1040 |
+
def test_relative_path_under_symlinked_subdir_rejected(self):
|
| 1041 |
+
# Old code compared raw strings, so a cwd-relative in-registry path
|
| 1042 |
+
# never matched the absolute VOICES_DIR scope and skipped the walk.
|
| 1043 |
+
import os as _os
|
| 1044 |
+
t = Path(tempfile.mkdtemp())
|
| 1045 |
+
(t / "real" / ".voices").mkdir(parents=True)
|
| 1046 |
+
(t / "elsewhere").mkdir()
|
| 1047 |
+
try:
|
| 1048 |
+
(t / "real" / ".voices" / "sub").symlink_to(t / "elsewhere")
|
| 1049 |
+
except OSError:
|
| 1050 |
+
self.skipTest("symlinks unsupported here")
|
| 1051 |
+
ctx_mod = sys.modules["vlib.ctx"]
|
| 1052 |
+
old, old_ctx = voice.VOICES_DIR, ctx_mod.VOICES_DIR
|
| 1053 |
+
prev_cwd = _os.getcwd()
|
| 1054 |
+
try:
|
| 1055 |
+
voice.VOICES_DIR = t / "real" / ".voices"
|
| 1056 |
+
ctx_mod.VOICES_DIR = t / "real" / ".voices"
|
| 1057 |
+
_os.chdir(t)
|
| 1058 |
+
with self.assertRaises(SystemExit):
|
| 1059 |
+
voice._assert_not_symlink("real/.voices/sub/evil")
|
| 1060 |
+
finally:
|
| 1061 |
+
_os.chdir(prev_cwd)
|
| 1062 |
+
voice.VOICES_DIR = old
|
| 1063 |
+
ctx_mod.VOICES_DIR = old_ctx
|
| 1064 |
+
|
| 1065 |
+
def test_scope_boundaries(self):
|
| 1066 |
+
# Legit nested registry path: silent. Prefix-adjacent sibling
|
| 1067 |
+
# (".voices2"-style, outside but string-matching): refused, as before.
|
| 1068 |
+
# Genuinely unrelated user path: untouched.
|
| 1069 |
+
(self.vdir / "myadd").mkdir()
|
| 1070 |
+
voice._assert_not_symlink(self.vdir / "myadd" / "voice.json")
|
| 1071 |
+
sib = self.vdir.parent / (self.vdir.name + "2")
|
| 1072 |
+
sib.mkdir(exist_ok=True)
|
| 1073 |
+
with self.assertRaises(SystemExit):
|
| 1074 |
+
voice._assert_not_symlink(sib / "voice.json")
|
| 1075 |
+
voice._assert_not_symlink(Path(tempfile.mkdtemp()) / "voice.json")
|
| 1076 |
+
|
| 1077 |
+
def test_topup_write_refuses_symlink(self):
|
| 1078 |
+
d = Path(tempfile.mkdtemp())
|
| 1079 |
+
hdr = {"a": {"dtype": "F16", "shape": [4], "data_offsets": [0, 8]}}
|
| 1080 |
+
hj = json.dumps(hdr).encode()
|
| 1081 |
+
real = d / "base.safetensors"
|
| 1082 |
+
real.write_bytes(struct.pack("<Q", len(hj)) + hj + b"\x00" * 8)
|
| 1083 |
+
link = d / "link.safetensors"
|
| 1084 |
+
try:
|
| 1085 |
+
link.symlink_to(real)
|
| 1086 |
+
except OSError:
|
| 1087 |
+
self.skipTest("symlinks unsupported here")
|
| 1088 |
+
with self.assertRaises(SystemExit):
|
| 1089 |
+
voice._cache_topup_write(link, [])
|
| 1090 |
+
|
| 1091 |
if __name__ == "__main__":
|
| 1092 |
unittest.main()
|
vlib/cache.py
CHANGED
|
@@ -5,11 +5,12 @@ import json
|
|
| 5 |
import os
|
| 6 |
import struct
|
| 7 |
from vlib import ctx
|
| 8 |
-
from vlib.ui import _fail, _now_iso, _step, _warn
|
| 9 |
from vlib.net import _cleanup_tmp, _safe_id, http_get_json
|
| 10 |
from vlib.tensors import _is_ggml_quant, _write_safetensors_streaming, read_safetensors, write_safetensors
|
| 11 |
from vlib.sources import _base_paths, open_source
|
| 12 |
from vlib.fetch import _fetch_tensor, _resolve_output_name, _source_arch
|
|
|
|
| 13 |
from vlib.registry import _assert_not_symlink
|
| 14 |
|
| 15 |
|
|
@@ -118,6 +119,32 @@ def _cache_topup_write(base_file, staged):
|
|
| 118 |
return new_hdr
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
def _ensure_base_tensors(base_id, names, args=None):
|
| 122 |
"""Base cache with per-tensor trust: top-ups what's missing, self-heals partial caches.
|
| 123 |
All fetches pin one revision so a repo update mid-cache can't mix commits. Returns base_file."""
|
|
@@ -199,6 +226,7 @@ def _ensure_base_tensors_locked(base_id, base_file, base_json, want):
|
|
| 199 |
elif not verified:
|
| 200 |
_warn(" Base cache lineage unverified (offline?) β proceeding, mixed revisions possible.")
|
| 201 |
rev = _base_rev(base_id, base_file)
|
|
|
|
| 202 |
missing = _cache_validate(base_file, want)
|
| 203 |
if not missing:
|
| 204 |
return base_file
|
|
|
|
| 5 |
import os
|
| 6 |
import struct
|
| 7 |
from vlib import ctx
|
| 8 |
+
from vlib.ui import _fail, _now_iso, _say, _step, _warn
|
| 9 |
from vlib.net import _cleanup_tmp, _safe_id, http_get_json
|
| 10 |
from vlib.tensors import _is_ggml_quant, _write_safetensors_streaming, read_safetensors, write_safetensors
|
| 11 |
from vlib.sources import _base_paths, open_source
|
| 12 |
from vlib.fetch import _fetch_tensor, _resolve_output_name, _source_arch
|
| 13 |
+
from vlib.delta import _tied_embed_name, _voice_head_name
|
| 14 |
from vlib.registry import _assert_not_symlink
|
| 15 |
|
| 16 |
|
|
|
|
| 119 |
return new_hdr
|
| 120 |
|
| 121 |
|
| 122 |
+
def _tied_head_want(base_id, rev, want):
|
| 123 |
+
"""Reroute a wanted voice head onto the base's tied embed tensor.
|
| 124 |
+
Voice untied + base tied: the base can never serve the head name, so
|
| 125 |
+
want the embed instead (the delta pairs them later). Returns the
|
| 126 |
+
want list, order-preserved and deduplicated. Anything unresolvable
|
| 127 |
+
(offline, base has the head, base not tied) returns want unchanged
|
| 128 |
+
and downstream fails exactly as before."""
|
| 129 |
+
head = _voice_head_name(want)
|
| 130 |
+
if head is None or head not in want:
|
| 131 |
+
return want
|
| 132 |
+
try:
|
| 133 |
+
src = open_source(base_id, rev=rev)
|
| 134 |
+
names = [n for n in src.names() if n != "delta.voice.marker"]
|
| 135 |
+
except SystemExit:
|
| 136 |
+
raise
|
| 137 |
+
except Exception:
|
| 138 |
+
return want
|
| 139 |
+
if head in names:
|
| 140 |
+
return want
|
| 141 |
+
emb = _tied_embed_name(names)
|
| 142 |
+
if emb is None:
|
| 143 |
+
return want
|
| 144 |
+
_say(f" β Base head is tied; caching '{emb}' for the head delta.")
|
| 145 |
+
return list(dict.fromkeys(emb if n == head else n for n in want))
|
| 146 |
+
|
| 147 |
+
|
| 148 |
def _ensure_base_tensors(base_id, names, args=None):
|
| 149 |
"""Base cache with per-tensor trust: top-ups what's missing, self-heals partial caches.
|
| 150 |
All fetches pin one revision so a repo update mid-cache can't mix commits. Returns base_file."""
|
|
|
|
| 226 |
elif not verified:
|
| 227 |
_warn(" Base cache lineage unverified (offline?) β proceeding, mixed revisions possible.")
|
| 228 |
rev = _base_rev(base_id, base_file)
|
| 229 |
+
want = _tied_head_want(base_id, rev, want)
|
| 230 |
missing = _cache_validate(base_file, want)
|
| 231 |
if not missing:
|
| 232 |
return base_file
|
vlib/delta.py
CHANGED
|
@@ -9,11 +9,57 @@ import time
|
|
| 9 |
from vlib.ui import _fail, _noninteractive, _progress_pref, _say, _transform, _warn
|
| 10 |
from vlib.net import _fmt_eta
|
| 11 |
from vlib.tensors import _is_delta_safetensors, _is_ggml_quant, decode_to_f32, dtype_bytes, read_safetensors
|
| 12 |
-
from vlib.sources import _detect_base, _normalize_hf_repo, _public_source, open_source
|
| 13 |
from vlib.fetch import _resolve_output_name, _source_arch
|
| 14 |
from vlib.registry import _assert_not_symlink, _iter_voices, _read_meta
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
def _choose_voice(args):
|
| 18 |
voices = []
|
| 19 |
for d in _iter_voices():
|
|
@@ -162,10 +208,18 @@ def _compute_delta(voice_path, base_path, delta_folder, delta_name, args=None):
|
|
| 162 |
_cand = None
|
| 163 |
ordered = ([_cand] if _cand in v_names else []) + [n for n in v_names if n != _cand]
|
| 164 |
missing = [n for n in ordered if n not in b_names]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
if missing:
|
| 166 |
shown = ", ".join(missing[:5]) + ("β¦" if len(missing) > 5 else "")
|
| 167 |
_fail(f" β {len(missing)} voice tensor(s) have no match in base: {shown}. Delta needs same names on both sides.")
|
| 168 |
-
pairs = [(n, n) for n in ordered]
|
| 169 |
_check_delta_gate(args, voice_path, base_path)
|
| 170 |
_assert_not_symlink(delta_folder)
|
| 171 |
delta_folder.mkdir(parents=True, exist_ok=True)
|
|
@@ -247,10 +301,18 @@ def _compute_delta(voice_path, base_path, delta_folder, delta_name, args=None):
|
|
| 247 |
if not v_keys:
|
| 248 |
_fail(" β No tensors found for delta.")
|
| 249 |
missing = [k for k in v_keys if k not in b_tensors]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
if missing:
|
| 251 |
shown = ", ".join(missing[:5]) + ("β¦" if len(missing) > 5 else "")
|
| 252 |
_fail(f" β {len(missing)} voice tensor(s) have no match in base: {shown}. Delta needs same names on both sides.")
|
| 253 |
-
pairs = [(k, k) for k in v_keys]
|
| 254 |
_check_delta_gate(args, voice_path, base_path)
|
| 255 |
with open(voice_path, "rb") as f:
|
| 256 |
v_hs = struct.unpack("<Q", f.read(8))[0]
|
|
|
|
| 9 |
from vlib.ui import _fail, _noninteractive, _progress_pref, _say, _transform, _warn
|
| 10 |
from vlib.net import _fmt_eta
|
| 11 |
from vlib.tensors import _is_delta_safetensors, _is_ggml_quant, decode_to_f32, dtype_bytes, read_safetensors
|
| 12 |
+
from vlib.sources import _detect_base, _normalize_hf_repo, _public_source, open_source, OUTPUT_ALIASES, EMBEDDING_ALIASES
|
| 13 |
from vlib.fetch import _resolve_output_name, _source_arch
|
| 14 |
from vlib.registry import _assert_not_symlink, _iter_voices, _read_meta
|
| 15 |
|
| 16 |
|
| 17 |
+
_EMBED_SUFFIXES = ("embed_tokens.weight", "word_embeddings.weight", "wte.weight", "token_embd.weight")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _tied_embed_name(b_names):
|
| 21 |
+
"""Base-side tied counterpart: the embed tensor doubling as output head.
|
| 22 |
+
Returns the embed name only when the base is truly headless (no head
|
| 23 |
+
tensor at all); otherwise None."""
|
| 24 |
+
for n in b_names:
|
| 25 |
+
if n in OUTPUT_ALIASES:
|
| 26 |
+
return None
|
| 27 |
+
if n.endswith("weight") and ("lm_head" in n or "output_layer" in n or "embed_out" in n):
|
| 28 |
+
return None
|
| 29 |
+
for n in b_names:
|
| 30 |
+
if n in EMBEDDING_ALIASES or n.endswith(_EMBED_SUFFIXES):
|
| 31 |
+
return n
|
| 32 |
+
return None
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _voice_head_name(v_names):
|
| 36 |
+
"""Voice-side output head: exact alias first, head-like pattern after."""
|
| 37 |
+
for n in v_names:
|
| 38 |
+
if n in OUTPUT_ALIASES:
|
| 39 |
+
return n
|
| 40 |
+
for n in v_names:
|
| 41 |
+
if n.endswith("weight") and ("lm_head" in n or "output_layer" in n or "embed_out" in n):
|
| 42 |
+
return n
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _tied_head_pair(head, b_names, vshape, bshape_of):
|
| 47 |
+
"""Voice untied + base tied: pair the voice head with the base's tied
|
| 48 |
+
embed tensor (it serves as both input and output projection, so it is
|
| 49 |
+
the only valid counterpart). Returns the base name, or None when the
|
| 50 |
+
base is not tied. Shapes must match exactly."""
|
| 51 |
+
if head is None:
|
| 52 |
+
return None
|
| 53 |
+
b_emb = _tied_embed_name(b_names)
|
| 54 |
+
if b_emb is None:
|
| 55 |
+
return None
|
| 56 |
+
bshape = tuple(bshape_of(b_emb))
|
| 57 |
+
if tuple(vshape) != bshape:
|
| 58 |
+
_fail(f" β Voice head '{head}' {list(vshape)} doesn't fit tied base '{b_emb}' {list(bshape)} β layouts differ.")
|
| 59 |
+
_say(f" β Base head is tied; '{head}' deltas against '{b_emb}'.")
|
| 60 |
+
return b_emb
|
| 61 |
+
|
| 62 |
+
|
| 63 |
def _choose_voice(args):
|
| 64 |
voices = []
|
| 65 |
for d in _iter_voices():
|
|
|
|
| 208 |
_cand = None
|
| 209 |
ordered = ([_cand] if _cand in v_names else []) + [n for n in v_names if n != _cand]
|
| 210 |
missing = [n for n in ordered if n not in b_names]
|
| 211 |
+
fallback = {}
|
| 212 |
+
v_head = _voice_head_name(v_names)
|
| 213 |
+
if v_head in missing:
|
| 214 |
+
b = _tied_head_pair(v_head, b_names, tuple(v_src.ref(v_head).shape),
|
| 215 |
+
lambda n: tuple(b_src.ref(n).shape))
|
| 216 |
+
if b is not None:
|
| 217 |
+
fallback[v_head] = b
|
| 218 |
+
missing = [n for n in missing if n not in fallback]
|
| 219 |
if missing:
|
| 220 |
shown = ", ".join(missing[:5]) + ("β¦" if len(missing) > 5 else "")
|
| 221 |
_fail(f" β {len(missing)} voice tensor(s) have no match in base: {shown}. Delta needs same names on both sides.")
|
| 222 |
+
pairs = [(n, fallback.get(n, n)) for n in ordered]
|
| 223 |
_check_delta_gate(args, voice_path, base_path)
|
| 224 |
_assert_not_symlink(delta_folder)
|
| 225 |
delta_folder.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 301 |
if not v_keys:
|
| 302 |
_fail(" β No tensors found for delta.")
|
| 303 |
missing = [k for k in v_keys if k not in b_tensors]
|
| 304 |
+
fallback = {}
|
| 305 |
+
v_head = _voice_head_name(v_keys)
|
| 306 |
+
if v_head in missing:
|
| 307 |
+
b = _tied_head_pair(v_head, list(b_tensors.keys()), tuple(v_tensors[v_head]["shape"]),
|
| 308 |
+
lambda n: tuple(b_tensors[n]["shape"]))
|
| 309 |
+
if b is not None:
|
| 310 |
+
fallback[v_head] = b
|
| 311 |
+
missing = [k for k in missing if k not in fallback]
|
| 312 |
if missing:
|
| 313 |
shown = ", ".join(missing[:5]) + ("β¦" if len(missing) > 5 else "")
|
| 314 |
_fail(f" β {len(missing)} voice tensor(s) have no match in base: {shown}. Delta needs same names on both sides.")
|
| 315 |
+
pairs = [(k, fallback.get(k, k)) for k in v_keys]
|
| 316 |
_check_delta_gate(args, voice_path, base_path)
|
| 317 |
with open(voice_path, "rb") as f:
|
| 318 |
v_hs = struct.unpack("<Q", f.read(8))[0]
|
voice-cli.tar.gz
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ebae7075d10181b2d6a5a640d8b064358dc3cbdac34a46f91444c00f01c2abf
|
| 3 |
+
size 70556
|
voice.py
CHANGED
|
@@ -49,7 +49,7 @@ from vlib.tensors import _validate_safetensors_header, read_safetensors, write_s
|
|
| 49 |
from vlib.sources import OUTPUT_ALIASES, EMBEDDING_ALIASES, _family, _nested, detect_output_tensor, largest_2d, _hf_config, _hf_license, _hf_model_index, _normalize_hf_repo, _base_paths, _hf_api_base_model, _hf_mergekit_base, _detect_base, TensorRef, SafetensorsSource, GgufSource, open_source, _source_from_repo, _pick_gguf, _sanitize_voice_name, _NAME_RULES, _public_source, _slugify_voice_name
|
| 50 |
from vlib.fetch import _source_arch, _resolve_output_name, _stream_safetensors_tensor, _fetch_tensor, _stream_gguf_tensor, _fetch_pack, _write_fetched, _write_voicepack_json
|
| 51 |
from vlib.registry import log_operation, _atomic_jsonl_append, _write_outbox, _append_report, _iter_voices, _registry_resolve, _read_meta, _registry_name_exists, _registry_folder, _assert_not_symlink, _write_voice, _resolve_name, _out_path, _stem, _check_overwrite
|
| 52 |
-
from vlib.cache import _base_rev, _cache_validate, _cache_topup_write, _ensure_base_tensors, _ensure_base_tensors_locked, _cache_base, _load_safetensors_or_gguf_f32
|
| 53 |
from vlib.delta import _choose_voice, _resolve_base, _delta_gate, _check_delta_gate, _compute_delta
|
| 54 |
from vlib.reports import _hash_file, _sanitize_note, _sanitize_reporter, _prompt_share, _push_reports_batch, _push_report, _last_operation, cmd_report
|
| 55 |
from vlib.cmds_voices import cmd_get, cmd_cast, cmd_delta, cmd_add
|
|
|
|
| 49 |
from vlib.sources import OUTPUT_ALIASES, EMBEDDING_ALIASES, _family, _nested, detect_output_tensor, largest_2d, _hf_config, _hf_license, _hf_model_index, _normalize_hf_repo, _base_paths, _hf_api_base_model, _hf_mergekit_base, _detect_base, TensorRef, SafetensorsSource, GgufSource, open_source, _source_from_repo, _pick_gguf, _sanitize_voice_name, _NAME_RULES, _public_source, _slugify_voice_name
|
| 50 |
from vlib.fetch import _source_arch, _resolve_output_name, _stream_safetensors_tensor, _fetch_tensor, _stream_gguf_tensor, _fetch_pack, _write_fetched, _write_voicepack_json
|
| 51 |
from vlib.registry import log_operation, _atomic_jsonl_append, _write_outbox, _append_report, _iter_voices, _registry_resolve, _read_meta, _registry_name_exists, _registry_folder, _assert_not_symlink, _write_voice, _resolve_name, _out_path, _stem, _check_overwrite
|
| 52 |
+
from vlib.cache import _base_rev, _cache_validate, _cache_topup_write, _ensure_base_tensors, _ensure_base_tensors_locked, _cache_base, _load_safetensors_or_gguf_f32, _tied_head_want
|
| 53 |
from vlib.delta import _choose_voice, _resolve_base, _delta_gate, _check_delta_gate, _compute_delta
|
| 54 |
from vlib.reports import _hash_file, _sanitize_note, _sanitize_reporter, _prompt_share, _push_reports_batch, _push_report, _last_operation, cmd_report
|
| 55 |
from vlib.cmds_voices import cmd_get, cmd_cast, cmd_delta, cmd_add
|
voice.pyz
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7568525f51fe2bbebeec82b6dd709bb3fb62f798e52485c0cf63290a7f5e4cec
|
| 3 |
+
size 260239
|