onw-Image 0.3: Panther Lake ~77 -> ~48 s per 1024 image (fused RMSNorm kernels, FlashAttention-2, async zero-copy blocks; same model files)
6b8ea09 verified Download zit/engine.py from ryugyosoft/onw-image: direct link, hf CLI and curl.
- Browser
- Download file 24 kB
-
https://huggingface.co/ryugyosoft/onw-image/resolve/main/zit/engine.py
- Command line
-
hf download hf://ryugyosoft/onw-image/zit/engine.py
-
curl -L -o engine.py https://huggingface.co/ryugyosoft/onw-image/resolve/main/zit/engine.py
24 kB
| """onw-Image runtime v2: conv-form DiT graphs (zit.cdit) on an OpenVINO device; host does the small parts. | |
| eng = Engine(conv_dir, device="NPU", size=(1024, 1024)) | |
| lat = eng.denoise("prompt", seed=42) | |
| NPU graphs (DiT blocks, text encoder segments, VAE) are compiled once into exported blobs in <conv>/npu_blobs3 and | |
| imported from there (with NPU_TURBO). With onw's NPU kit (zit.npukit) the blobs are weightless: the weights are rebuilt | |
| on the NPU from the model's .bin files at import, so the blobs hold the graphs only (0.1 kept the weights in every | |
| blob: ~36 GB next to a 10 GB model on Panther Lake). | |
| """ | |
| import json, os, time | |
| import numpy as np | |
| import openvino as ov | |
| from . import host, dit, te, cdit, npukit | |
| # (0.1: npu_blobs2 = full blobs, and an OpenVINO CACHE_DIR for the text encoder and VAE; ONW_IMAGE_BLOB_DIR: tests) | |
| BLOB_DIR = os.environ.get("ONW_IMAGE_BLOB_DIR", "npu_blobs3") | |
| CHAT = "<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n" | |
| D = cdit.D | |
| CAP_BUCKETS = (64, 256, 512) # caption token buckets (each needs its own compiled DiT set; Japanese ~1 token/char) | |
| def cap_bucket(n): | |
| for b in CAP_BUCKETS: | |
| if n <= b: | |
| return b | |
| raise ValueError("prompt too long (max 512 tokens)") | |
| def f16(a): | |
| return np.ascontiguousarray(a, np.float16) | |
| class HostData(dict): | |
| """Host-side arrays: <conv>/host/<key>.npy memory-mapped on first use (the 778 MB token table is only touched | |
| row by row), else everything from host.npz.""" | |
| def __init__(self, conv): | |
| super().__init__() | |
| self.dir = os.path.join(conv, "host") | |
| if not os.path.isdir(self.dir): | |
| self.update(dict(np.load(os.path.join(conv, "host.npz")))) | |
| self.dir = None | |
| def __missing__(self, k): | |
| if self.dir is None: | |
| raise KeyError(k) | |
| v = np.load(os.path.join(self.dir, k.replace(":", "@") + ".npy"), mmap_mode="r") | |
| self[k] = v | |
| return v | |
| def __contains__(self, k): | |
| return dict.__contains__(self, k) or (self.dir is not None and | |
| os.path.exists(os.path.join(self.dir, k.replace(":", "@") + ".npy"))) | |
| def _arr(x): | |
| """numpy view of a block output (an ov.Tensor with zero-copy chaining, else already numpy).""" | |
| return x.data if isinstance(x, ov.Tensor) else x | |
| class Engine: | |
| def __init__(self, conv, device="NPU", size=(1024, 1024), cache_dir=None, log=print, te_device=None, props=None, | |
| lazy=None): | |
| self.conv, self.device, self.log = conv, device, log | |
| self.lazy = (device == "CPU") if lazy is None else lazy # CPU checks: compile per use (f32 copies are big) | |
| self.Hh = HostData(conv) | |
| self.V = dict(np.load(os.path.join(conv, "dit2_vec.npz"))) | |
| self.core = npukit.core() | |
| if cache_dir and device != "NPU": # (NPU graphs: exported blobs, see _npu_request) | |
| self.core.set_property({"CACHE_DIR": cache_dir}) | |
| self.props = dict(props or {}) | |
| self.export_only = False # precompile children: compile + export, no import | |
| # block outputs stay in the plugin's tensors and feed the next block directly ($ONW_IMAGE_ZEROCOPY=0: copies) | |
| self.zero_copy = device == "NPU" and os.environ.get("ONW_IMAGE_ZEROCOPY", "1") != "0" | |
| self._wmodels = {} # graph key -> its ov.Model (a weightless import reads it) | |
| # exported DiT blobs are imported with NPU_TURBO (higher NPU clock: ~15% faster convolutions on Panther Lake); | |
| # it is a runtime setting at import - passed to compile_model it would change the cache key and recompile | |
| turbo = os.environ.get("ONW_IMAGE_TURBO", "1") != "0" | |
| self.import_props = {**self.props, "NPU_TURBO": "YES"} if turbo else dict(self.props) | |
| self.te_device = te_device or device | |
| self.H, self.W = int(size[0]), int(size[1]) | |
| self.ht, self.wt = self.H // 16, self.W // 16 | |
| self.n_img = self.ht * self.wt | |
| self._te, self._blocks, self._bucket, self._models = {}, {}, None, [] | |
| # zit.cdit_u builds (dit2/uniform.json): every block of a kind is the same graph -> one weightless blob per kind | |
| self.uniform = os.path.exists(os.path.join(conv, "dit2", "uniform.json")) | |
| # 0.3: uniform blocks are rebuilt in memory in a faster graph form (zit.cdit_u2: same weight constants, no new | |
| # model files): G4 (default; FlashAttention-2 with the kit's compiler) or G3; $ONW_IMAGE_GRAPH=0.2 keeps the | |
| # graphs as stored | |
| gf = os.environ.get("ONW_IMAGE_GRAPH", "g4") | |
| self.graph_form = gf if self.uniform and gf in ("g3", "g4") else "" | |
| from tokenizers import Tokenizer | |
| self.tok = Tokenizer.from_file(os.path.join(conv, "tokenizer.json")) | |
| # ---------- compile ---------- | |
| def _shapes(self, streams, sfxs=("",)): | |
| sh = {} | |
| for s, (h, w) in streams.items(): | |
| n = h * w | |
| sh[f"x_{s}"] = [1, D, h, w] | |
| sh[f"mask_{s}"] = [1, 1, 1, n] | |
| for x in sfxs: | |
| sh.update({f"{t}_{s}{x}": [1, 1, n, cdit.HD] for t in ("cq", "sq", "ck", "sk")}) | |
| for x in sfxs: | |
| sh.update({n + x: [1, D, 1, 1] for n in ("m_a", "g_a", "m_f", "g_f")}) | |
| return sh | |
| def groups(self): | |
| """[[layer names]] when the main layers are chained in groups (dit2/groups.json), else one per layer.""" | |
| p = os.path.join(self.conv, "dit2", "groups.json") | |
| return json.load(open(p)) if os.path.exists(p) else [[f"layers.{i}"] for i in range(30)] | |
| def _graph_name(self, gi, grp): | |
| return f"group.{gi}" if len(grp) > 1 or grp[0].startswith("group") else grp[0] | |
| def blob_path(self, b, shapes): | |
| """Exported NPU blob for graph b at these shapes (<conv>/npu_blobs3/<b>_<HxW>[_<HxW>].blob); uniform builds | |
| with weightless blobs: one per kind (<kind>.all_...: the 30 main layers, the noise / context refiners).""" | |
| tag = "_".join(f"{v[2]}x{v[3]}" for k, v in sorted(shapes.items()) if k.startswith("x_")) | |
| if self.uniform and npukit.weightless() and "." in b: | |
| b = b.split(".")[0] + ".all" | |
| form = f"-{self.graph_form}" if self.graph_form else "" | |
| return os.path.join(self.conv, BLOB_DIR, f"{b}{form}_{tag}.blob") | |
| def _npu_request(self, path, model, device, cfg=None): | |
| """Infer request for an NPU graph from its exported blob `path` - compiled and exported first when missing | |
| (weightless with the kit). Imports carry NPU_TURBO (a runtime setting: given to the compile it would be part | |
| of the blob). export_only: compile + export, return None.""" | |
| cfg = dict(self.props if cfg is None else cfg) | |
| if npukit.blob_file(path) is None: | |
| flash = self.graph_form == "g4" and "_s" not in os.path.basename(path) and not os.path.basename(path).startswith("vae") | |
| if flash: # (the kit's compiler: FlashAttention-2 for the DiT's SDPA) | |
| os.environ["ONW_FLASH_SDPA"] = "1" | |
| try: | |
| npukit.compile_blob(self.core, model, device, cfg, path, log=self.log) | |
| finally: | |
| if flash: | |
| os.environ.pop("ONW_FLASH_SDPA", None) | |
| if self.export_only: | |
| return None | |
| self._wmodels[path] = model | |
| imp = {**cfg, **{k: v for k, v in self.import_props.items() if k == "NPU_TURBO"}} | |
| return npukit.import_blob(self.core, model, device, imp, path).create_infer_request() | |
| def _compile_block(self, b, streams, n_sub=1): | |
| sh = self._shapes(streams, [f"@{j}" for j in range(n_sub)] if n_sub > 1 else ("",)) | |
| xml = os.path.join(self.conv, "dit2", b + ".xml") | |
| if self.graph_form: | |
| from . import cdit_u2 | |
| m = cdit_u2.build(xml, cdit_u2.G4 if self.graph_form == "g4" else cdit_u2.G3, self.core) | |
| else: | |
| m = self.core.read_model(xml) | |
| m.reshape(sh) | |
| if self.device == "NPU": | |
| return self._npu_request(self.blob_path(b, sh), m, self.device) | |
| self._models.append(m) # CPU graphs may reference the model constants | |
| return self.core.compile_model(m, self.device, self.props).create_infer_request() | |
| def load_blocks(self, bucket): | |
| if self._bucket == bucket: | |
| return | |
| self._blocks, self._models = {}, [] | |
| t = time.time() | |
| img, cap = (self.ht, self.wt), (1, bucket) | |
| self._groups = self.groups() | |
| self._nsub = {} | |
| self._streams = {**{f"noise_refiner.{i}": {"img": img} for i in range(2)}, | |
| **{f"context_refiner.{i}": {"cap": cap} for i in range(2)}} | |
| for gi, grp in enumerate(self._groups): | |
| name = self._graph_name(gi, grp) | |
| self._streams[name] = {"img": img, "cap": cap} | |
| self._nsub[name] = len(grp) if name.startswith("group") else 1 | |
| if self.lazy: | |
| self._blocks = {b: None for b in self._streams} | |
| self._bucket = bucket | |
| return | |
| for i in range(2): | |
| self._blocks[f"noise_refiner.{i}"] = self._compile_block(f"noise_refiner.{i}", {"img": img}) | |
| self._blocks[f"context_refiner.{i}"] = self._compile_block(f"context_refiner.{i}", {"cap": cap}) | |
| for gi, grp in enumerate(self._groups): | |
| name = self._graph_name(gi, grp) | |
| self._blocks[name] = self._compile_block(name, {"img": img, "cap": cap}, self._nsub[name]) | |
| self.log(f" {name} ready ({time.time()-t:.0f}s)") | |
| self._bucket = bucket | |
| # ---------- text ---------- | |
| def _te_paths(self): | |
| d = os.path.join(self.conv, "te_seg") | |
| if os.path.isdir(d): | |
| n = len([f for f in os.listdir(d) if f.endswith(".xml")]) | |
| return [os.path.join(d, f"te_{i}.xml") for i in range(n)] | |
| return [os.path.join(self.conv, "te", "te.xml")] | |
| def encode_compile(self, S): | |
| """Compile (or import from the cache) the text encoder segments for prompt bucket S.""" | |
| if S in self._te: | |
| return | |
| reqs = [] | |
| for path in self._te_paths(): | |
| m = self.core.read_model(path) | |
| m.reshape({"emb": [1, S, te.HID], "cos": [1, S, 1, 64], "sin": [1, S, 1, 64], "mask": [1, 1, S, S]}) | |
| if self.te_device == "NPU": | |
| name = os.path.splitext(os.path.basename(path))[0] | |
| reqs.append(self._npu_request(os.path.join(self.conv, BLOB_DIR, f"{name}_s{S}.blob"), m, "NPU")) | |
| else: | |
| reqs.append(self.core.compile_model(m, self.te_device, self.props).create_infer_request()) | |
| del m | |
| self._te = {S: reqs} | |
| def embed(self, ids): | |
| """Token embeddings [n, 2560] f32: from the INT8 table (te.embed_tokens_q + per-row scale te.embed_tokens_s, | |
| 0.39 GB) when the model has it, else the f16 one (0.78 GB).""" | |
| if "te.embed_tokens_q" in self.Hh: | |
| return self.Hh["te.embed_tokens_q"][ids].astype(np.float32) * \ | |
| self.Hh["te.embed_tokens_s"][ids].astype(np.float32)[:, None] | |
| return self.Hh["te.embed_tokens"][ids].astype(np.float32) | |
| def encode(self, prompt): | |
| """Prompt -> caption features [n, 2560] (text encoder segments compiled per prompt bucket, then kept until | |
| free_text_encoder()).""" | |
| ids = self.tok.encode(CHAT.format(prompt)).ids[:512] | |
| S = next(b for b in (64, 128, 256, 512) if len(ids) <= b) | |
| self.encode_compile(S) | |
| x = np.zeros((1, S, te.HID), np.float32) | |
| x[0, :len(ids)] = self.embed(ids) | |
| cos, sin, mask = te.host_inputs(ids, S) | |
| for rq in self._te[S]: | |
| rq.infer({"emb": x, "cos": cos, "sin": sin, "mask": mask}) | |
| x = rq.get_output_tensor(0).data.astype(np.float32).copy() | |
| return x[0, :len(ids)].copy() | |
| def free_text_encoder(self): | |
| self._te = {} | |
| import gc | |
| gc.collect() | |
| # ---------- DiT ---------- | |
| def _run(self, b, feed): | |
| return self._finish(b, self._start(b, feed)) | |
| def _start(self, b, feed): | |
| """Start block b on its own infer request (async on the NPU: the host prepares the next block meanwhile).""" | |
| rq = self._blocks[b] | |
| if rq is None: | |
| self._models = [] | |
| rq = self._compile_block(b, self._streams[b], self._nsub.get(b, 1)) | |
| if self.zero_copy: | |
| # NPU: a block's output tensor (the plugin's own device-visible buffer) goes straight into the next block - | |
| # no copy out to numpy and back in (~60 MB per block at 1024); numpy inputs are shared, not copied | |
| rq.start_async(feed, share_inputs=True) | |
| else: | |
| rq.infer(feed) | |
| return rq | |
| def _finish(self, b, rq): | |
| if self.zero_copy: | |
| rq.wait() | |
| out = {o.get_any_name(): rq.get_tensor(o) for o in rq.model_outputs} | |
| else: | |
| out = {o.get_any_name(): rq.get_tensor(o).data.copy() for o in rq.model_outputs} | |
| if self.lazy: | |
| self._models = [] | |
| return out | |
| def _vecs(self, b, mods=None): | |
| V = self.V | |
| a_in, a_out, f_in, f_out = (V[f"{b}:{k}"] for k in ("a_in", "a_out", "f_in", "f_out")) | |
| if mods is not None: | |
| s1, g1, s2, g2 = (m.ravel() for m in mods) | |
| a_in, a_out, f_in, f_out = a_in * s1, a_out * g1, f_in * s2, f_out * g2 | |
| out = {n: f16(v.reshape(1, D, 1, 1)) for n, v in zip(("m_a", "g_a", "m_f", "g_f"), (a_in, a_out, f_in, f_out))} | |
| for lin in ("qkv", "out", "w1", "w3", "w2"): # zit.cdit_u shared weights: the caption stream's guards | |
| v = V.get(f"{b}:p_{lin}") | |
| if v is not None: | |
| out[f"p_{lin}"] = f16(v.reshape(1, -1, 1, 1)) | |
| return out | |
| def _sublayers(self, b): | |
| if b.startswith("group"): | |
| return self._groups[int(b.split(".")[1])] | |
| return [b] | |
| def _tables(self, b, s, ang): | |
| cq, sq, ck, sk = cdit.rope_tables(ang, self.V[f"{b}:q_g"], self.V[f"{b}:k_g"]) | |
| return {f"cq_{s}": cq, f"sq_{s}": sq, f"ck_{s}": ck, f"sk_{s}": sk} | |
| def prepare(self, cap_feats): | |
| Hh = self.Hh | |
| n = len(cap_feats) | |
| cp = host.cap_padded_len(n) | |
| bucket = cap_bucket(cp) | |
| self.load_blocks(bucket) | |
| c = dit.rms_np(cap_feats, Hh["cap_embedder.0.weight"], 1e-5) | |
| c = c @ Hh["cap_embedder.1.weight"].T + Hh["cap_embedder.1.bias"] | |
| cap = np.empty((bucket, D), np.float32) | |
| cap[:n] = c | |
| cap[n:] = Hh["cap_pad_token"] # pad and bucket rows (bucket rows: masked keys, never zero) | |
| img_ids, cap_ids = host.position_ids(n, self.ht, self.wt) | |
| ids_b = np.zeros((bucket, 3), np.int64) | |
| ids_b[:cp] = cap_ids | |
| a_img, a_cap = host.rope_angles(img_ids), host.rope_angles(ids_b) | |
| mask_cap = np.zeros((1, 1, 1, bucket), np.float16) | |
| mask_cap[..., cp:] = cdit.NEG | |
| mask_img = np.zeros((1, 1, 1, self.n_img), np.float16) | |
| self.cp = cp | |
| self.const = {} | |
| self._fbc, self.fbcache_skipped, self.fbcache_log = None, 0, [] # (First Block Cache: per image) | |
| for b in self._blocks: | |
| f = {} | |
| subs = self._sublayers(b) | |
| for j, lb in enumerate(subs): | |
| sfx = f"@{j}" if len(subs) > 1 or b.startswith("group") else "" | |
| if not b.startswith("context"): | |
| f.update({k + sfx: v for k, v in self._tables(lb, "img", a_img).items()}) | |
| if not b.startswith("noise"): | |
| f.update({k + sfx: v for k, v in self._tables(lb, "cap", a_cap).items()}) | |
| if not b.startswith("context"): | |
| f["mask_img"] = mask_img | |
| if not b.startswith("noise"): | |
| f["mask_cap"] = mask_cap | |
| self.const[b] = f | |
| k = cdit.RESID_K | |
| x = f16((cap / k["context_refiner"]).T.reshape(1, D, 1, bucket)) | |
| for i in range(2): | |
| b = f"context_refiner.{i}" | |
| x = self._run(b, dict(self.const[b], x_cap=x, **self._vecs(b)))["out_cap"] | |
| self.cap = f16(_arr(x).astype(np.float32) * (k["context_refiner"] / k["layers"])) | |
| def dit(self, lat, t, trace=None): | |
| Hh, k = self.Hh, cdit.RESID_K | |
| temb = dit.t_embed(Hh, host.timestep_freq(t)) | |
| xi = host.patchify(lat) @ Hh["all_x_embedder.2-1.weight"].T + Hh["all_x_embedder.2-1.bias"] | |
| xi = f16((xi / k["noise_refiner"]).T.reshape(1, D, self.ht, self.wt)) | |
| for i in range(2): | |
| b = f"noise_refiner.{i}" | |
| xi = self._run(b, dict(self.const[b], x_img=xi, **self._vecs(b, dit.modulation(Hh, b + ".", temb))))["out_img"] | |
| if trace is not None: | |
| trace[b] = _arr(xi)[0].reshape(D, -1).T.astype(np.float32) * k["noise_refiner"] | |
| xi = f16(_arr(xi).astype(np.float32) * (k["noise_refiner"] / k["layers"])) | |
| xc = self.cap | |
| # First Block Cache ($ONW_IMAGE_FBCACHE = threshold, e.g. 0.1): after the first main block, when its residual | |
| # changed little since the previous step, the other blocks are skipped and their previous residual reused | |
| thr = float(os.environ.get("ONW_IMAGE_FBCACHE", "0") or 0) | |
| fb = getattr(self, "_fbc", None) if thr > 0 else None | |
| pending = None # (block, request) running on the NPU while the host prepares the next block | |
| x_first_in = xi | |
| skipped = False | |
| for gi, grp in enumerate(self._groups + [None]): | |
| vecs = {} | |
| if grp is not None: | |
| b = self._graph_name(gi, grp) | |
| for j, lb in enumerate(grp): | |
| sfx = f"@{j}" if b.startswith("group") else "" | |
| vecs.update({n + sfx: v for n, v in self._vecs(lb, dit.modulation(Hh, lb + ".", temb)).items()}) | |
| if pending is not None: | |
| r = self._finish(*pending) | |
| xi, xc = r["out_img"], r["out_cap"] | |
| pending = None | |
| if trace is not None: | |
| trace[self._groups[gi - 1][-1]] = np.concatenate([_arr(xi)[0].reshape(D, -1).T, _arr(xc)[0].reshape(D, -1).T[:self.cp]]).astype(np.float32) * k["layers"] | |
| if thr > 0 and gi == 1: | |
| r1 = _arr(xi).astype(np.float32) - _arr(x_first_in).astype(np.float32) | |
| x_first = _arr(xi).astype(np.float32) | |
| if fb is not None and fb["r1"].shape == r1.shape: | |
| change = float(np.abs(r1 - fb["r1"]).mean() / max(np.abs(fb["r1"]).mean(), 1e-12)) | |
| self.fbcache_log = getattr(self, "fbcache_log", []) + [change] | |
| if change < thr: | |
| xi = f16(x_first + fb["rest"]) | |
| self.fbcache_skipped = getattr(self, "fbcache_skipped", 0) + 1 | |
| skipped = True # (the reference residuals stay: errors do not drift) | |
| break | |
| if grp is None: | |
| break | |
| pending = (b, self._start(b, dict(self.const[b], x_img=xi, x_cap=xc, **vecs))) | |
| if thr > 0 and not skipped: | |
| self._fbc = {"r1": r1, "rest": _arr(xi).astype(np.float32) - x_first} | |
| out = dit.final_layer(Hh, _arr(xi)[0].reshape(D, -1).T.astype(np.float32) * k["layers"], temb) | |
| return host.unpatchify(out, self.H // 8, self.W // 8) | |
| # ---------- VAE ---------- | |
| # On the NPU the VAE decodes in tiles of VAE_TILE x VAE_TILE latents (512x512 pixels) overlapping by >= VAE_OVERLAP, | |
| # blended linearly: ONE compiled graph for every image size, and a 1024x1024 graph alone was 189 MB of NPU task | |
| # descriptors and a 14-minute compile (512: 52 MB, 99 s). ONW_IMAGE_VAE_TILE=0: the whole image at once. | |
| VAE_TILE, VAE_OVERLAP = 64, 16 | |
| def _tile_starts(n, t, ov): | |
| """Evenly spread tile starts covering [0, n) with tiles of t and overlaps >= ov.""" | |
| if n <= t: | |
| return [0] | |
| k = -(-(n - t) // (t - ov)) + 1 | |
| return [round(i * (n - t) / (k - 1)) for i in range(k)] | |
| def _vae_graph(self, dev, h, w): | |
| key = (dev, h, w) | |
| if getattr(self, "_vae_key", None) != key: | |
| m = self.core.read_model(os.path.join(self.conv, "vae", "vae_decoder.xml")) | |
| m.reshape({0: [1, 16, h, w]}) | |
| self._vae_m = m | |
| if dev == "NPU": | |
| bp = os.path.join(self.conv, BLOB_DIR, f"vae_{h}x{w}.blob") | |
| self._vae = self._npu_request(bp, m, "NPU", {"INFERENCE_PRECISION_HINT": "f16"}) | |
| else: | |
| self._vae = self.core.compile_model(m, dev).create_infer_request() | |
| self._vae_key = key if self._vae is not None else None | |
| return self._vae | |
| def decode(self, lat, device=None): | |
| """latent [16,H/8,W/8] -> PIL image (Flux VAE decoder IR in <conv>/vae).""" | |
| from PIL import Image | |
| dev = device or getattr(self, "vae_device", None) or self.device | |
| _, lh, lw = lat.shape | |
| t = self.VAE_TILE | |
| tiled = os.environ.get("ONW_IMAGE_VAE_TILE", "1") # (force: tiles on any device - checks) | |
| if (dev != "NPU" and tiled != "force") or tiled == "0" or (lh <= t and lw <= t): | |
| rq = self._vae_graph(dev, lh, lw) | |
| if rq is None: # (export_only) | |
| return None | |
| rq.infer({0: lat[None].astype(np.float32)}) | |
| img = rq.get_output_tensor(0).data[0] | |
| else: | |
| rq = self._vae_graph(dev, t, t) | |
| if rq is None: | |
| return None | |
| ys, xs = self._tile_starts(lh, t, self.VAE_OVERLAP), self._tile_starts(lw, t, self.VAE_OVERLAP) | |
| acc = np.zeros((3, lh * 8, lw * 8), np.float32) | |
| wsum = np.zeros((1, lh * 8, lw * 8), np.float32) | |
| def ramp(starts, i, n): | |
| """Weights along one axis for tile i: linear ramps over the overlaps with its neighbours.""" | |
| w = np.ones(t * 8, np.float32) | |
| s = starts[i] | |
| if i > 0: | |
| ov = (starts[i - 1] + t - s) * 8 | |
| w[:ov] = np.minimum(w[:ov], (np.arange(ov) + 0.5) / ov) | |
| if i < len(starts) - 1: | |
| ov = (s + t - starts[i + 1]) * 8 | |
| w[t * 8 - ov:] = np.minimum(w[t * 8 - ov:], (ov - np.arange(ov) - 0.5) / ov) | |
| return w | |
| for iy, y0 in enumerate(ys): | |
| wy = ramp(ys, iy, lh) | |
| for ix, x0 in enumerate(xs): | |
| wx = ramp(xs, ix, lw) | |
| rq.infer({0: np.ascontiguousarray(lat[None, :, y0:y0 + t, x0:x0 + t], np.float32)}) | |
| tile = rq.get_output_tensor(0).data[0].astype(np.float32) | |
| w2 = wy[:, None] * wx[None, :] | |
| acc[:, y0 * 8:(y0 + t) * 8, x0 * 8:(x0 + t) * 8] += tile * w2 | |
| wsum[:, y0 * 8:(y0 + t) * 8, x0 * 8:(x0 + t) * 8] += w2 | |
| img = acc / np.maximum(wsum, 1e-6) | |
| img = ((np.clip(img, -1, 1) + 1) * 127.5).round().astype(np.uint8).transpose(1, 2, 0) | |
| return Image.fromarray(img) | |
| def denoise(self, prompt, seed=42, steps=9, latents=None, callback=None): | |
| t0 = time.time() | |
| cap = self.encode(prompt) | |
| self.free_text_encoder() # 16 GB machines: the text encoder and the DiT do not fit together | |
| t1 = time.time() | |
| self.prepare(cap) | |
| if latents is None: | |
| latents = np.random.default_rng(seed).standard_normal((16, self.H // 8, self.W // 8)).astype(np.float32) | |
| lat = latents.astype(np.float32) | |
| sig = host.sigmas(steps) | |
| for i in range(steps): | |
| lat = host.euler(lat, self.dit(lat, host.model_t(sig[i])), sig[i], sig[i + 1]) | |
| if callback: | |
| callback(i, steps, lat) | |
| self.timing = {"text": t1 - t0, "denoise": time.time() - t1} | |
| return lat | |