onwAMD / onw /dl.py
ryugyosoft's picture
onw AMD 0.1.3: model file check, installer restart, download progress
00a5163 verified
Raw History Blame Contribute Delete
5.98 kB
"""Download one model (or just FastFlowLM) as its own process: the manager can cancel it by ending the process, and
a crash inside a download cannot take the tray down. Progress goes to a small JSON file the manager reads.
usage (internal): python -m onw.dl KEY STATUS_FILE a model ("...-FLM"): FastFlowLM first when missing,
`flm pull`, then every file checked against flm's hashes
python -m onw.dl --repair KEY STATUS_FILE the model's damaged files fetched again (after a check)
python -m onw.dl --verify KEY STATUS_FILE just the check; the result (damaged files) in STATUS_FILE
python -m onw.dl --engine STATUS_FILE just FastFlowLM
"""
import json, os, re, subprocess, sys, time
from . import app, lemonade
class Status:
def __init__(self, path):
self.path, self.last = path, 0.0
def __call__(self, frac, text, force=False, **extra):
if not force and time.time() - self.last < 0.5:
return
self.last = time.time()
with open(self.path + ".tmp", "w", encoding="utf8") as f:
json.dump({"frac": round(frac, 4), "text": text, **extra}, f, ensure_ascii=False)
os.replace(self.path + ".tmp", self.path)
def engine(status):
if not lemonade.up_to_date():
lemonade.install(status)
status(1.0, "FastFlowLM " + lemonade.VERSION, force=True)
def pull(m, status, cfg, force=False):
"""`flm pull <checkpoint>`; progress from its "[FLM] Downloading X/Y: file" lines (lemonade: flm_download)."""
p = subprocess.Popen(lemonade.flm_pull_command(m["checkpoint"], force), stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, env=lemonade.flm_env(cfg), text=True, encoding="utf8",
errors="replace", **lemonade.NO_WINDOW)
# flm prints "Downloading 2/6: model.q4nx" per file and, with \r, "Downloading: 44.6% (85.7MB / 191.9MB)" while a
# file comes down: the bytes give the progress (one big weights file would otherwise sit at 0% for minutes)
file_pat = re.compile(r"Downloading\s+(\d+)/(\d+):\s*(.*)")
byte_pat = re.compile(r"Downloading:\s*([\d.]+)%\s*\(([\d.]+)\s*([KMGT]?B)\s*/\s*([\d.]+)\s*([KMGT]?B)\)")
unit = {"B": 1, "KB": 2**10, "MB": 2**20, "GB": 2**30, "TB": 2**40}
total = lemonade.flm_download_size(m["checkpoint"]) # the files' sizes (the footprint is memory, not download)
done = cur = cur_total = 0.0 # bytes of the finished files, of the current one
i = n = 0
name = ""
def report():
whole = max(total, done + cur_total, 1)
frac = min((done + cur) / whole, 0.99)
status(frac, f"{(done + cur) / 2**30:.1f} / {whole / 2**30:.1f} GB ({frac * 100:.0f}%)"
+ (f" · {i}/{n} {name[:40]}" if n else ""))
for line in p.stdout:
print(line, end="", flush=True)
line = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", line)
g = file_pat.search(line)
if g:
done, cur, cur_total = done + cur_total, 0.0, 0.0 # the previous file is complete
i, n, name = int(g.group(1)), int(g.group(2)), g.group(3).strip()
report()
continue
g = byte_pat.search(line)
if g:
cur = float(g.group(2)) * unit.get(g.group(3).upper(), 1)
cur_total = float(g.group(4)) * unit.get(g.group(5).upper(), 1)
report()
if p.wait() != 0:
sys.exit(f"flm pull failed (exit code {p.returncode})")
def verify(m, status, cfg):
"""The model's files against flm's hashes. -> damaged file names ([] intact, None: no hashes to check)."""
status(0.0, "checking…", force=True)
bad = lemonade.flm_verify(m["checkpoint"], cfg,
lambda frac, text: status(frac, f"checking {text} ({frac * 100:.0f}%)"))
print(f"[onw] verify {m['checkpoint']}: " + ("no hashes" if bad is None else ", ".join(bad) or "intact"), flush=True)
return bad
def repair(m, bad, status, cfg):
"""Fetch the damaged files again: remove them so that `flm pull` downloads them; if flm still counts the model
as complete, pull it whole with --force. Exits when the files are still damaged."""
for name, path, *_ in lemonade.flm_files(m["checkpoint"], cfg):
if name in bad and os.path.exists(path):
os.remove(path)
pull(m, status, cfg)
bad = verify(m, status, cfg)
if bad:
pull(m, status, cfg, force=True)
bad = verify(m, status, cfg)
if bad:
sys.exit("downloaded files are damaged (checked against FastFlowLM's hashes): " + ", ".join(bad))
def model_entry(key, cfg):
ok, msg = lemonade.flm_validate(cfg)
if not ok:
sys.exit(f"FastFlowLM NPU check failed: {msg}")
m = lemonade.flm_models(cfg).get(key)
if m is None:
sys.exit(f"FastFlowLM has no model {key}")
return m
def main():
args = sys.argv[1:]
if args[0] == "--engine":
engine(Status(args[1]))
return
if args[0] in ("--verify", "--repair"):
mode, key, status = args[0], args[1], Status(args[2])
else:
mode, key, status = "--download", args[0], Status(args[1])
cfg = app.load_config()
status(0.0, "…", force=True)
if mode == "--verify":
m = lemonade.flm_models(cfg).get(key)
if m is None:
sys.exit(f"FastFlowLM has no model {key}")
bad = verify(m, status, cfg)
status(1.0, "done", force=True, bad=bad)
return
engine(lambda frac, text, force=False: status(0.0, text, force))
m = model_entry(key, cfg)
if mode == "--download":
pull(m, status, cfg)
bad = verify(m, status, cfg) # (--repair: finds the damaged files again, here)
if bad:
repair(m, bad, status, cfg)
status(1.0, "done", force=True, bad=[])
if __name__ == "__main__":
main()