pylolm 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.
- pylolm/__init__.py +36 -0
- pylolm/__main__.py +168 -0
- pylolm/_download.py +184 -0
- pylolm/_lib.py +155 -0
- pylolm/errors.py +25 -0
- pylolm/llama.py +441 -0
- pylolm/native/CMakeLists.txt +28 -0
- pylolm/native/LLAMA_CPP_REF +1 -0
- pylolm/native/build_android_ndk.sh +54 -0
- pylolm/native/build_termux.sh +45 -0
- pylolm/native/pylolm_shim.c +316 -0
- pylolm/py.typed +0 -0
- pylolm-0.1.0.dist-info/METADATA +174 -0
- pylolm-0.1.0.dist-info/RECORD +18 -0
- pylolm-0.1.0.dist-info/WHEEL +5 -0
- pylolm-0.1.0.dist-info/entry_points.txt +2 -0
- pylolm-0.1.0.dist-info/licenses/LICENSE +21 -0
- pylolm-0.1.0.dist-info/top_level.txt +1 -0
pylolm/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""pylolm (LoLm): run GGUF language models on-device on Android, from Python.
|
|
2
|
+
|
|
3
|
+
An Android-friendly take on llama-cpp-python, built on llama.cpp and designed for
|
|
4
|
+
Kivy, Flet and Termux.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .errors import (
|
|
8
|
+
ContextOverflowError,
|
|
9
|
+
DownloadError,
|
|
10
|
+
GenerationError,
|
|
11
|
+
LibraryNotFoundError,
|
|
12
|
+
ModelLoadError,
|
|
13
|
+
PyLoLmError,
|
|
14
|
+
)
|
|
15
|
+
from .llama import Llama
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
__version__ = _version("pylolm")
|
|
22
|
+
except PackageNotFoundError: # running from a source checkout
|
|
23
|
+
__version__ = "0.0.0+local"
|
|
24
|
+
except ImportError: # pragma: no cover
|
|
25
|
+
__version__ = "0.0.0+local"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Llama",
|
|
29
|
+
"PyLoLmError",
|
|
30
|
+
"LibraryNotFoundError",
|
|
31
|
+
"ModelLoadError",
|
|
32
|
+
"ContextOverflowError",
|
|
33
|
+
"DownloadError",
|
|
34
|
+
"GenerationError",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
pylolm/__main__.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Command line: ``python -m pylolm {doctor,get-libs,build-native,chat}`` (also ``pylolm``)."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__, _lib
|
|
11
|
+
from .errors import DownloadError, PyLoLmError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cmd_doctor(_args) -> int:
|
|
15
|
+
print("pylolm %s" % __version__)
|
|
16
|
+
print("python %s on %s (%s)" % (platform.python_version(), platform.system(), platform.machine()))
|
|
17
|
+
print("search order for native libraries:")
|
|
18
|
+
for d in _lib.candidate_dirs():
|
|
19
|
+
found = (d / _lib.SHIM_NAME).is_file()
|
|
20
|
+
print(" [%s] %s" % ("x" if found else " ", d))
|
|
21
|
+
try:
|
|
22
|
+
_lib.load()
|
|
23
|
+
except PyLoLmError as exc:
|
|
24
|
+
print("\nNOT READY: %s" % exc)
|
|
25
|
+
return 1
|
|
26
|
+
print("\nOK: native libraries loaded from %s" % _lib.loaded_dir())
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cmd_get_libs(args) -> int:
|
|
31
|
+
from . import _download
|
|
32
|
+
|
|
33
|
+
pending = [False] # a progress line is on screen without a trailing newline
|
|
34
|
+
|
|
35
|
+
def progress(done, total):
|
|
36
|
+
pending[0] = True
|
|
37
|
+
mb = done / 1e6
|
|
38
|
+
text = " %.1f / %.1f MB" % (mb, total / 1e6) if total else " %.1f MB" % mb
|
|
39
|
+
print("\r" + text, end="", flush=True)
|
|
40
|
+
|
|
41
|
+
def log(message):
|
|
42
|
+
if pending[0]:
|
|
43
|
+
print()
|
|
44
|
+
pending[0] = False
|
|
45
|
+
print(message)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
_download.get_libs(
|
|
49
|
+
version=args.version,
|
|
50
|
+
repo=args.repo,
|
|
51
|
+
base_url=args.base_url,
|
|
52
|
+
force=args.force,
|
|
53
|
+
progress=progress,
|
|
54
|
+
log=log,
|
|
55
|
+
)
|
|
56
|
+
except DownloadError as exc:
|
|
57
|
+
if pending[0]:
|
|
58
|
+
print()
|
|
59
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
print("\nNext: pylolm doctor")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_build_native(_args) -> int:
|
|
66
|
+
script = Path(__file__).resolve().parent / "native" / "build_termux.sh"
|
|
67
|
+
if not script.is_file():
|
|
68
|
+
print("build script not found: %s" % script, file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
return subprocess.call(["bash", str(script)])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_chat(args) -> int:
|
|
74
|
+
from .llama import Llama
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
llm = Llama(args.model, n_ctx=args.n_ctx, n_threads=args.threads)
|
|
78
|
+
except (PyLoLmError, FileNotFoundError) as exc:
|
|
79
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
history = []
|
|
83
|
+
if args.system:
|
|
84
|
+
history.append({"role": "system", "content": args.system})
|
|
85
|
+
print("pylolm chat - /reset clears history, /exit quits.\n")
|
|
86
|
+
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
user = input("you> ").strip()
|
|
90
|
+
except (EOFError, KeyboardInterrupt):
|
|
91
|
+
print()
|
|
92
|
+
break
|
|
93
|
+
if not user:
|
|
94
|
+
continue
|
|
95
|
+
if user == "/exit":
|
|
96
|
+
break
|
|
97
|
+
if user == "/reset":
|
|
98
|
+
history = history[:1] if args.system else []
|
|
99
|
+
llm.reset()
|
|
100
|
+
print("(history cleared)")
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
history.append({"role": "user", "content": user})
|
|
104
|
+
reply = []
|
|
105
|
+
t0 = time.time()
|
|
106
|
+
n = 0
|
|
107
|
+
try:
|
|
108
|
+
print("bot> ", end="", flush=True)
|
|
109
|
+
for chunk in llm.create_chat_completion(
|
|
110
|
+
history, max_tokens=args.max_tokens, temperature=args.temp, stream=True
|
|
111
|
+
):
|
|
112
|
+
text = chunk["choices"][0]["delta"].get("content")
|
|
113
|
+
if text:
|
|
114
|
+
n += 1
|
|
115
|
+
reply.append(text)
|
|
116
|
+
print(text, end="", flush=True)
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
llm.stop()
|
|
119
|
+
print("\n(interrupted)")
|
|
120
|
+
except PyLoLmError as exc:
|
|
121
|
+
print("\nerror: %s" % exc)
|
|
122
|
+
history.pop()
|
|
123
|
+
continue
|
|
124
|
+
dt = max(time.time() - t0, 1e-6)
|
|
125
|
+
print("\n[%d chunks, %.1f/s]\n" % (n, n / dt))
|
|
126
|
+
history.append({"role": "assistant", "content": "".join(reply)})
|
|
127
|
+
|
|
128
|
+
llm.close()
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main(argv=None) -> int:
|
|
133
|
+
parser = argparse.ArgumentParser(prog="pylolm", description="Local LLMs on Android, from Python")
|
|
134
|
+
parser.add_argument("--version", action="version", version="pylolm " + __version__)
|
|
135
|
+
sub = parser.add_subparsers(dest="command")
|
|
136
|
+
|
|
137
|
+
sub.add_parser("doctor", help="check that the native libraries can be found and loaded")
|
|
138
|
+
sub.add_parser("build-native", help="build llama.cpp + the shim on this device (slow, Termux)")
|
|
139
|
+
|
|
140
|
+
get = sub.add_parser("get-libs", help="download the prebuilt native libraries (fast)")
|
|
141
|
+
get.add_argument("--version", default=None, help="release to download (default: this pylolm)")
|
|
142
|
+
get.add_argument("--repo", default=None, help="GitHub repo as owner/name")
|
|
143
|
+
get.add_argument("--base-url", default=None, help="download from this URL instead of GitHub")
|
|
144
|
+
get.add_argument("--force", action="store_true", help="replace existing libraries")
|
|
145
|
+
|
|
146
|
+
chat = sub.add_parser("chat", help="interactive chat in the terminal")
|
|
147
|
+
chat.add_argument("model", help="path to a .gguf file")
|
|
148
|
+
chat.add_argument("--n-ctx", type=int, default=2048)
|
|
149
|
+
chat.add_argument("--threads", type=int, default=None)
|
|
150
|
+
chat.add_argument("--system", default=None, help="system prompt")
|
|
151
|
+
chat.add_argument("--max-tokens", type=int, default=512)
|
|
152
|
+
chat.add_argument("--temp", type=float, default=0.7)
|
|
153
|
+
|
|
154
|
+
args = parser.parse_args(argv)
|
|
155
|
+
handlers = {
|
|
156
|
+
"doctor": cmd_doctor,
|
|
157
|
+
"get-libs": cmd_get_libs,
|
|
158
|
+
"build-native": cmd_build_native,
|
|
159
|
+
"chat": cmd_chat,
|
|
160
|
+
}
|
|
161
|
+
if args.command not in handlers:
|
|
162
|
+
parser.print_help()
|
|
163
|
+
return 0
|
|
164
|
+
return handlers[args.command](args)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
sys.exit(main())
|
pylolm/_download.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Download the prebuilt native libraries from a GitHub Release (``pylolm get-libs``).
|
|
2
|
+
|
|
3
|
+
Safety: the archive is only used if its SHA-256 matches the checksum file published next to
|
|
4
|
+
it, and extraction only accepts plain ``*.so`` files (no paths, links or other entry types).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import shutil
|
|
11
|
+
import tarfile
|
|
12
|
+
import tempfile
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable, List, Optional
|
|
17
|
+
|
|
18
|
+
from .errors import DownloadError
|
|
19
|
+
|
|
20
|
+
ASSET = "pylolm-libs-android-arm64-v8a.tar.gz"
|
|
21
|
+
DEFAULT_REPO = "auraway/pylolm"
|
|
22
|
+
SHIM_NAME = "libpylolm_shim.so"
|
|
23
|
+
MAX_BYTES = 300 * 1024 * 1024 # refuse absurdly large downloads
|
|
24
|
+
|
|
25
|
+
Progress = Callable[[int, int], None]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_dest() -> Path:
|
|
29
|
+
"""Same place the loader and `pylolm build-native` use."""
|
|
30
|
+
env = os.environ.get("PYLOLM_LIB_DIR")
|
|
31
|
+
return Path(env).expanduser() if env else Path.home() / ".pylolm" / "lib"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def release_base_url(version: str, repo: Optional[str] = None) -> str:
|
|
35
|
+
repo = repo or os.environ.get("PYLOLM_RELEASE_REPO") or DEFAULT_REPO
|
|
36
|
+
return "https://github.com/%s/releases/download/v%s" % (repo, version)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _fetch(url: str, target: Path, progress: Optional[Progress] = None) -> None:
|
|
40
|
+
req = urllib.request.Request(url, headers={"User-Agent": "pylolm-get-libs"})
|
|
41
|
+
try:
|
|
42
|
+
with urllib.request.urlopen(req, timeout=30) as resp, open(str(target), "wb") as out:
|
|
43
|
+
total = int(resp.headers.get("Content-Length") or 0)
|
|
44
|
+
done = 0
|
|
45
|
+
while True:
|
|
46
|
+
block = resp.read(1 << 16)
|
|
47
|
+
if not block:
|
|
48
|
+
break
|
|
49
|
+
done += len(block)
|
|
50
|
+
if done > MAX_BYTES:
|
|
51
|
+
raise DownloadError("download is larger than expected; aborting")
|
|
52
|
+
out.write(block)
|
|
53
|
+
if progress:
|
|
54
|
+
progress(done, total)
|
|
55
|
+
except urllib.error.HTTPError as exc:
|
|
56
|
+
if exc.code == 404:
|
|
57
|
+
raise DownloadError(
|
|
58
|
+
"not found (404): %s\n"
|
|
59
|
+
"Has this version been released with prebuilt libraries yet? "
|
|
60
|
+
"Use `pylolm build-native` instead." % url
|
|
61
|
+
)
|
|
62
|
+
raise DownloadError("HTTP error %d for %s" % (exc.code, url))
|
|
63
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
64
|
+
raise DownloadError("could not download %s: %s" % (url, exc))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _sha256_of(path: Path) -> str:
|
|
68
|
+
h = hashlib.sha256()
|
|
69
|
+
with open(str(path), "rb") as f:
|
|
70
|
+
for block in iter(lambda: f.read(1 << 20), b""):
|
|
71
|
+
h.update(block)
|
|
72
|
+
return h.hexdigest()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _expected_sha256(path: Path) -> str:
|
|
76
|
+
try:
|
|
77
|
+
token = path.read_text().split()[0].lower()
|
|
78
|
+
except (IndexError, UnicodeDecodeError, OSError):
|
|
79
|
+
raise DownloadError("checksum file is empty or unreadable")
|
|
80
|
+
if len(token) != 64 or any(c not in "0123456789abcdef" for c in token):
|
|
81
|
+
raise DownloadError("checksum file does not contain a SHA-256 hash")
|
|
82
|
+
return token
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _safe_extract(archive: Path, out_dir: Path) -> List[str]:
|
|
86
|
+
"""Extract plain *.so files only, writing them ourselves (no tar path handling)."""
|
|
87
|
+
names: List[str] = []
|
|
88
|
+
try:
|
|
89
|
+
with tarfile.open(str(archive), "r:gz") as tar:
|
|
90
|
+
members = tar.getmembers()
|
|
91
|
+
if not members:
|
|
92
|
+
raise DownloadError("archive is empty")
|
|
93
|
+
for m in members:
|
|
94
|
+
name = m.name
|
|
95
|
+
if (
|
|
96
|
+
not m.isfile()
|
|
97
|
+
or os.path.basename(name) != name
|
|
98
|
+
or name.startswith(".")
|
|
99
|
+
or not name.endswith(".so")
|
|
100
|
+
):
|
|
101
|
+
raise DownloadError("unexpected entry in archive: %r" % name)
|
|
102
|
+
for m in members:
|
|
103
|
+
src = tar.extractfile(m)
|
|
104
|
+
if src is None:
|
|
105
|
+
raise DownloadError("could not read %r from archive" % m.name)
|
|
106
|
+
target = out_dir / m.name
|
|
107
|
+
with open(str(target), "wb") as dst:
|
|
108
|
+
shutil.copyfileobj(src, dst)
|
|
109
|
+
os.chmod(str(target), 0o755)
|
|
110
|
+
names.append(m.name)
|
|
111
|
+
except (tarfile.TarError, EOFError, OSError) as exc:
|
|
112
|
+
raise DownloadError("archive is corrupt: %s" % exc)
|
|
113
|
+
if SHIM_NAME not in names:
|
|
114
|
+
raise DownloadError("archive does not contain %s" % SHIM_NAME)
|
|
115
|
+
return names
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_libs(
|
|
119
|
+
version: Optional[str] = None,
|
|
120
|
+
repo: Optional[str] = None,
|
|
121
|
+
base_url: Optional[str] = None,
|
|
122
|
+
dest: Optional[Path] = None,
|
|
123
|
+
force: bool = False,
|
|
124
|
+
progress: Optional[Progress] = None,
|
|
125
|
+
log: Callable[[str], None] = print,
|
|
126
|
+
) -> Path:
|
|
127
|
+
"""Download, verify and install the prebuilt libraries. Returns the install directory."""
|
|
128
|
+
machine = platform.machine().lower()
|
|
129
|
+
if machine not in ("aarch64", "arm64") and not force:
|
|
130
|
+
raise DownloadError(
|
|
131
|
+
"the prebuilt libraries are for Android arm64 (aarch64), but this device "
|
|
132
|
+
"reports %r. Use --force to install anyway, or run `pylolm build-native`." % machine
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if base_url is None:
|
|
136
|
+
if version is None:
|
|
137
|
+
from . import __version__
|
|
138
|
+
|
|
139
|
+
if "+" in __version__:
|
|
140
|
+
raise DownloadError(
|
|
141
|
+
"this looks like a source checkout (version %s); pass --version X.Y.Z"
|
|
142
|
+
% __version__
|
|
143
|
+
)
|
|
144
|
+
version = __version__
|
|
145
|
+
base_url = release_base_url(version, repo)
|
|
146
|
+
base_url = base_url.rstrip("/")
|
|
147
|
+
|
|
148
|
+
dest = Path(dest) if dest else default_dest()
|
|
149
|
+
if (dest / SHIM_NAME).is_file() and not force:
|
|
150
|
+
raise DownloadError(
|
|
151
|
+
"native libraries are already installed in %s. Use --force to replace them." % dest
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
staging = Path(tempfile.mkdtemp(prefix=".pylolm-get-libs-", dir=str(dest.parent)))
|
|
156
|
+
try:
|
|
157
|
+
archive = staging / ASSET
|
|
158
|
+
checksum = staging / (ASSET + ".sha256")
|
|
159
|
+
|
|
160
|
+
log("Downloading %s ..." % ASSET)
|
|
161
|
+
_fetch("%s/%s" % (base_url, ASSET), archive, progress)
|
|
162
|
+
_fetch("%s/%s.sha256" % (base_url, ASSET), checksum)
|
|
163
|
+
|
|
164
|
+
log("Verifying checksum ...")
|
|
165
|
+
expected, actual = _expected_sha256(checksum), _sha256_of(archive)
|
|
166
|
+
if expected != actual:
|
|
167
|
+
raise DownloadError(
|
|
168
|
+
"checksum mismatch (expected %s..., got %s...); the file was NOT installed"
|
|
169
|
+
% (expected[:12], actual[:12])
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
unpacked = staging / "unpacked"
|
|
173
|
+
unpacked.mkdir()
|
|
174
|
+
names = _safe_extract(archive, unpacked)
|
|
175
|
+
|
|
176
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
for old in dest.glob("*.so"):
|
|
178
|
+
old.unlink()
|
|
179
|
+
for name in names:
|
|
180
|
+
shutil.move(str(unpacked / name), str(dest / name))
|
|
181
|
+
log("Installed %d libraries to %s" % (len(names), dest))
|
|
182
|
+
return dest
|
|
183
|
+
finally:
|
|
184
|
+
shutil.rmtree(str(staging), ignore_errors=True)
|
pylolm/_lib.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Locate and load the native libraries, and declare the ctypes signatures of the shim."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterator, List
|
|
8
|
+
|
|
9
|
+
from .errors import LibraryNotFoundError
|
|
10
|
+
|
|
11
|
+
SHIM_ABI_VERSION = 1
|
|
12
|
+
SHIM_NAME = "libpylolm_shim.so"
|
|
13
|
+
|
|
14
|
+
# Loaded in this order with RTLD_GLOBAL so the dynamic linker finds them by soname.
|
|
15
|
+
# (name, required)
|
|
16
|
+
_DEPENDENCIES = [
|
|
17
|
+
("libggml-base.so", True),
|
|
18
|
+
("libggml-cpu.so", False), # only present when CPU variants are NOT used
|
|
19
|
+
("libggml.so", True),
|
|
20
|
+
("libllama.so", True),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# void callback(const char *piece, int len, void *user) -> int (non-zero = stop)
|
|
24
|
+
TOKEN_CB = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p)
|
|
25
|
+
|
|
26
|
+
_lock = threading.Lock()
|
|
27
|
+
_shim = None
|
|
28
|
+
_lib_dir = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def candidate_dirs() -> Iterator[Path]:
|
|
32
|
+
"""Directories searched for the native libraries, in priority order."""
|
|
33
|
+
env = os.environ.get("PYLOLM_LIB_DIR")
|
|
34
|
+
if env:
|
|
35
|
+
yield Path(env).expanduser()
|
|
36
|
+
yield Path(__file__).resolve().parent / "lib"
|
|
37
|
+
try:
|
|
38
|
+
yield Path.home() / ".pylolm" / "lib"
|
|
39
|
+
except RuntimeError: # no resolvable home directory
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_lib_dir() -> Path:
|
|
44
|
+
searched: List[str] = []
|
|
45
|
+
for d in candidate_dirs():
|
|
46
|
+
searched.append(str(d))
|
|
47
|
+
if (d / SHIM_NAME).is_file():
|
|
48
|
+
return d
|
|
49
|
+
raise LibraryNotFoundError(
|
|
50
|
+
"pylolm native libraries not found (looked for %s in: %s).\n"
|
|
51
|
+
" - Download the prebuilt libraries (fast): pylolm get-libs\n"
|
|
52
|
+
" - Or build them on this device (slow, ~30 min): pylolm build-native\n"
|
|
53
|
+
" - Or point PYLOLM_LIB_DIR at a folder containing the .so files."
|
|
54
|
+
% (SHIM_NAME, ", ".join(searched))
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _bind(shim: ctypes.CDLL) -> None:
|
|
59
|
+
c = ctypes
|
|
60
|
+
sampling = [
|
|
61
|
+
c.c_float, # temperature
|
|
62
|
+
c.c_float, # top_p
|
|
63
|
+
c.c_int, # top_k
|
|
64
|
+
c.c_float, # min_p
|
|
65
|
+
c.c_float, # repeat_penalty
|
|
66
|
+
c.c_uint32, # seed
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
shim.shim_abi_version.argtypes = []
|
|
70
|
+
shim.shim_abi_version.restype = c.c_int
|
|
71
|
+
|
|
72
|
+
shim.shim_set_verbose.argtypes = [c.c_int]
|
|
73
|
+
shim.shim_set_verbose.restype = None
|
|
74
|
+
|
|
75
|
+
shim.shim_init.argtypes = [c.c_char_p]
|
|
76
|
+
shim.shim_init.restype = c.c_int
|
|
77
|
+
|
|
78
|
+
shim.shim_load.argtypes = [c.c_char_p, c.c_int, c.c_int]
|
|
79
|
+
shim.shim_load.restype = c.c_void_p
|
|
80
|
+
|
|
81
|
+
shim.shim_stop.argtypes = [c.c_void_p]
|
|
82
|
+
shim.shim_stop.restype = None
|
|
83
|
+
|
|
84
|
+
shim.shim_reset.argtypes = [c.c_void_p]
|
|
85
|
+
shim.shim_reset.restype = None
|
|
86
|
+
|
|
87
|
+
shim.shim_n_ctx.argtypes = [c.c_void_p]
|
|
88
|
+
shim.shim_n_ctx.restype = c.c_int
|
|
89
|
+
|
|
90
|
+
shim.shim_count_tokens.argtypes = [c.c_void_p, c.c_char_p, c.c_int]
|
|
91
|
+
shim.shim_count_tokens.restype = c.c_int
|
|
92
|
+
|
|
93
|
+
shim.shim_last_stats.argtypes = [c.c_void_p, c.POINTER(c.c_int)]
|
|
94
|
+
shim.shim_last_stats.restype = None
|
|
95
|
+
|
|
96
|
+
shim.shim_chat.argtypes = (
|
|
97
|
+
[
|
|
98
|
+
c.c_void_p, # handle
|
|
99
|
+
c.POINTER(c.c_char_p), # roles
|
|
100
|
+
c.POINTER(c.c_char_p), # contents
|
|
101
|
+
c.c_int, # n_msgs
|
|
102
|
+
c.c_char_p, # template name or NULL
|
|
103
|
+
c.c_int, # max_tokens
|
|
104
|
+
]
|
|
105
|
+
+ sampling
|
|
106
|
+
+ [TOKEN_CB, c.c_void_p]
|
|
107
|
+
)
|
|
108
|
+
shim.shim_chat.restype = c.c_int
|
|
109
|
+
|
|
110
|
+
shim.shim_complete.argtypes = (
|
|
111
|
+
[c.c_void_p, c.c_char_p, c.c_int] + sampling + [TOKEN_CB, c.c_void_p]
|
|
112
|
+
)
|
|
113
|
+
shim.shim_complete.restype = c.c_int
|
|
114
|
+
|
|
115
|
+
shim.shim_free.argtypes = [c.c_void_p]
|
|
116
|
+
shim.shim_free.restype = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def load() -> ctypes.CDLL:
|
|
120
|
+
"""Load (once) and return the shim library."""
|
|
121
|
+
global _shim, _lib_dir
|
|
122
|
+
with _lock:
|
|
123
|
+
if _shim is not None:
|
|
124
|
+
return _shim
|
|
125
|
+
|
|
126
|
+
d = find_lib_dir()
|
|
127
|
+
try:
|
|
128
|
+
for name, required in _DEPENDENCIES:
|
|
129
|
+
path = d / name
|
|
130
|
+
if path.is_file():
|
|
131
|
+
ctypes.CDLL(str(path), mode=ctypes.RTLD_GLOBAL)
|
|
132
|
+
elif required:
|
|
133
|
+
raise LibraryNotFoundError("%s is missing from %s" % (name, d))
|
|
134
|
+
shim = ctypes.CDLL(str(d / SHIM_NAME))
|
|
135
|
+
except OSError as exc:
|
|
136
|
+
raise LibraryNotFoundError("failed to load native libraries from %s: %s" % (d, exc))
|
|
137
|
+
|
|
138
|
+
_bind(shim)
|
|
139
|
+
abi = shim.shim_abi_version()
|
|
140
|
+
if abi != SHIM_ABI_VERSION:
|
|
141
|
+
raise LibraryNotFoundError(
|
|
142
|
+
"native shim ABI %d does not match pylolm (expects %d); rebuild it with "
|
|
143
|
+
"`pylolm build-native`" % (abi, SHIM_ABI_VERSION)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
shim.shim_set_verbose(0)
|
|
147
|
+
shim.shim_init(str(d).encode())
|
|
148
|
+
_shim = shim
|
|
149
|
+
_lib_dir = d
|
|
150
|
+
return shim
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def loaded_dir():
|
|
154
|
+
"""Directory the libraries were loaded from, or None if not loaded yet."""
|
|
155
|
+
return _lib_dir
|
pylolm/errors.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Exceptions raised by pylolm."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PyLoLmError(Exception):
|
|
5
|
+
"""Base class for all pylolm errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LibraryNotFoundError(PyLoLmError):
|
|
9
|
+
"""The native libraries (libllama / libpylolm_shim) could not be found or loaded."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelLoadError(PyLoLmError):
|
|
13
|
+
"""The GGUF model could not be loaded."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContextOverflowError(PyLoLmError):
|
|
17
|
+
"""The prompt does not fit in the context window (n_ctx)."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GenerationError(PyLoLmError):
|
|
21
|
+
"""llama.cpp failed while generating."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DownloadError(PyLoLmError):
|
|
25
|
+
"""Downloading or installing the prebuilt native libraries failed."""
|