Download reference/workers.py from JohnP1/d1a-e4b-coreml: direct link, hf CLI and curl.
- Browser
- Download file 6.17 kB
-
https://huggingface.co/JohnP1/d1a-e4b-coreml/resolve/main/reference/workers.py
- Command line
-
hf download hf://JohnP1/d1a-e4b-coreml/reference/workers.py
-
curl -L -o workers.py https://huggingface.co/JohnP1/d1a-e4b-coreml/resolve/main/reference/workers.py
6.17 kB
| """One process per Core ML chunk (workaround: several long-context chunks in one process crash the Neural Engine runtime | |
| on the second request on this Mac, while each chunk alone, or all on the GPU, runs fine). The parent runs the host side | |
| (embeddings, masks, pointer head) and passes tensors along the chain through pipes. | |
| WorkerPipeline(export_dir, chunk_dir, L, units).probs(enc)""" | |
| import glob | |
| import os | |
| import multiprocessing as mp | |
| import re | |
| import shutil | |
| import sys | |
| import numpy as np | |
| import torch | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # d1a_coreml.py sits next to this file | |
| BUCKETS = tuple(int(x) for x in os.environ.get("D1A_COREML_BUCKETS", "512").split(",")) # each length is its own ANE program; fewer keeps the ANE within its memory | |
| def compiled_chunks(chunk_dir): | |
| """-> [(first layer, end layer, .mlmodelc, its .mlpackage)]. A released bundle's coreml/L512_<a>-<b>.mlpackage are | |
| compiled once, each as a one-function package (L512), into coreml/.compiled/; a build tree's compiled/*.mlmodelc | |
| (multifunction, one function per length) are used as they are.""" | |
| import coremltools as ct | |
| out = [] | |
| for f in sorted(glob.glob(f"{chunk_dir}/*.mlmodelc")): | |
| a, b = map(int, re.search(r"(\d+)-(\d+)\.mlmodelc$", f).groups()) | |
| out.append((a, b, f, f.replace("compiled/", "buckets/").replace(".mlmodelc", ".mlpackage"))) | |
| if out: | |
| return out | |
| os.makedirs(f"{chunk_dir}/.compiled", exist_ok=True) | |
| for src in sorted(glob.glob(f"{chunk_dir}/L*_*.mlpackage")): | |
| L, a, b = map(int, re.search(r"L(\d+)_(\d+)-(\d+)\.mlpackage$", src).groups()) | |
| dst = f"{chunk_dir}/.compiled/{a:02d}-{b:02d}.mlmodelc" | |
| if not os.path.exists(dst): | |
| d = ct.utils.MultiFunctionDescriptor(); d.add_function(src, src_function_name="main", target_function_name=f"L{L}") | |
| d.default_function_name = f"L{L}"; tmp = f"{chunk_dir}/.compiled/{a:02d}-{b:02d}.mlpackage" | |
| ct.utils.save_multifunction(d, tmp); shutil.move(ct.models.utils.compile_model(tmp), dst); shutil.rmtree(tmp) | |
| out.append((a, b, dst, src)) | |
| return out | |
| def _worker(path, spec_path, units, conn): | |
| import coremltools as ct | |
| cu = getattr(ct.ComputeUnit, units) | |
| # a precompiled .mlmodelc (compiled once by coremltools.models.utils.compile_model): loading each length function | |
| # from it never recompiles, where MLModel(.mlpackage) compiles the whole package again for every function | |
| models = {L: ct.models.CompiledMLModel(path, compute_units=cu, function_name=f"L{L}") for L in BUCKETS} | |
| spec = ct.utils.load_spec(spec_path) | |
| fns = {f.name: f for f in spec.description.functions} | |
| fn = fns.get(f"L{BUCKETS[0]}") or spec.description # a one-function package keeps its inputs in the description | |
| conn.send(([i.name for i in fn.input], [o.name for o in fn.output])) | |
| while True: | |
| msg = conn.recv() | |
| if msg is None: | |
| break | |
| L, feed = msg | |
| import time as _t | |
| t0 = _t.perf_counter() | |
| out = models[L].predict(feed) | |
| t1 = _t.perf_counter() | |
| res = {k: np.array(v, dtype=np.float16 if v.dtype == np.float32 else v.dtype, copy=True) for k, v in out.items()} | |
| res["_predict_ms"] = np.array((t1 - t0) * 1000) | |
| conn.send(res) | |
| class WorkerPipeline: | |
| def __init__(self, export_dir, chunk_dir, L=None, units="CPU_AND_NE"): | |
| from d1a_coreml import Export, HostEmbed | |
| self.ex = Export(export_dir) | |
| self.host = HostEmbed(self.ex) | |
| self.hq, self.hk = self.ex.head() | |
| self.T, self.L = self.ex.d1a["temperature"], L | |
| ctx = mp.get_context("spawn") | |
| self.chunks = [] | |
| for a, b, f, spec_path in compiled_chunks(chunk_dir): | |
| parent, child = ctx.Pipe() | |
| p = ctx.Process(target=_worker, args=(f, spec_path, units, child), daemon=True) | |
| p.start() | |
| self.chunks.append((a, b, p, parent)) | |
| self.io = getattr(self, "io", []) + [parent.recv()] # one at a time: concurrent first loads race in Core ML's compiler | |
| def close(self): | |
| for _, _, p, conn in self.chunks: | |
| conn.send(None); p.join(5) | |
| def hidden(self, enc): | |
| from d1a_coreml import masks_for | |
| self.last_predict_ms = [] | |
| n = len(enc["ids"]) | |
| L = next(b for b in BUCKETS if b >= n) # the smallest compiled length that fits | |
| ids = list(enc["ids"]) + [self.ex.d1a.get("pad_id", 0)] * (L - n) | |
| emb, ple = self.host(ids) | |
| mf, ms, pos = masks_for(enc, self.ex.tc.sliding_window, L=L) | |
| picks = list(enc["decide_idx"]) + [o for oo in enc["opt_idx"] for o in oo] | |
| pk = np.zeros(128, np.int32); pk[: len(picks)] = picks | |
| common = {"pos": pos.numpy().astype(np.int32), "mask_full": mf.clamp(min=-1e4).numpy().astype(np.float16), | |
| "mask_slide": ms.clamp(min=-1e4).numpy().astype(np.float16)} | |
| state = {"emb": emb.numpy().astype(np.float16), "ple_tok": ple.numpy().astype(np.float16), "picks": pk} | |
| for (a, b, p, conn), (ins, outs) in zip(self.chunks, self.io): | |
| conn.send((L, {k: (common[k] if k in common else state[k]) for k in ins})) | |
| out = conn.recv() | |
| self.last_predict_ms = getattr(self, "last_predict_ms", []) + [float(out.pop("_predict_ms"))] | |
| if "hidden" in out: | |
| return torch.tensor(out["hidden"][: len(picks)].astype(np.float32)) | |
| state["h_in"] = out["h"] | |
| if "ple_out" in out: | |
| state["ple"] = out["ple_out"] | |
| for k, v in out.items(): | |
| if k.endswith("_out") and k != "ple_out": | |
| state[k[: -len("_out")]] = v | |
| raise RuntimeError("no chunk returned hidden") | |
| def probs(self, enc): | |
| h = self.hidden(enc) | |
| Q = len(enc["decide_idx"]) | |
| out, j = [], Q | |
| with torch.no_grad(): | |
| for qi, oo in enumerate(enc["opt_idx"]): | |
| z = (self.hk(h[j: j + len(oo)]) @ self.hq(h[qi])) / (self.hq.out_features ** 0.5) | |
| out.append(torch.softmax(z / self.T, -1).numpy()); j += len(oo) | |
| return out | |