"""The vision checks' prepared asset views, kept on disk for every deepsearch-api process of the Space. Before each check, usdsearch's llm_client prepares the asset's stored view: it decodes the stored thumbnail, fits it to USDSEARCH_VISION_VALIDATION_IMAGE_MAX_PX and re-encodes it as base64 PNG, or JPEG with USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT=jpeg as on the Space (``Validation._encode_reference_images`` -> ``_encode_image``). That is most of a check's API CPU, and a stored view never changes. With ``CHECK_IMAGE_CACHE_DIR`` set (a Space Variable, pipeline/config.py), each view ``_encode_reference_images`` prepares is kept in ``//.b64``: a hit reads the file; a miss prepares the view as before and writes it (a temp file, then ``os.replace``), so the API processes share one copy. ```` is a digest of everything else that can shape a view: the validation config, llm_client's validation module, Pillow and its codecs. Query images are never cached. Each file carries its length and CRC-32. Any error (unreadable, unwritable, full disk, a damaged file) falls back to preparing the view, so a check never fails here, and a damaged file is written again. The views stay under MAX_BYTES and never leave less than MIN_FREE on the disk, which the Space's index, media and caches share: past either, new views are prepared per check, as without the cache. Unset or empty, nothing is imported or installed and checks run exactly as usdsearch's own code. check_image_cache.pth, in the venv's site-packages, imports this module in every /app/.venv python (PYTHONPATH is unset for them) while the variable is set. The import only arms a hook that wraps llm_client's Validation when, and if, the process imports it. A Validation without the wrapped methods (a future wheel) logs one warning and runs unchanged. ``python -m check_image_cache`` fills the cache once the index is in (supervisord-gpu.conf's [program:check-image-cache]): every image of the image cache index, loaded as a check loads it and prepared through the same wrapped path, so its files are the ones a check would write. It first removes what an earlier start left (other settings' directories, stale temp files). ``--self-test`` is the image build's check (Dockerfile.hf-space-gpu). """ import functools import hashlib import json import logging import os import re import shutil import sys import threading import time import weakref import zlib ENV = "CHECK_IMAGE_CACHE_DIR" TARGET = "llm_client.validation.validation" MAX_BYTES = 2_000_000_000 # 448-px views: ~28 KB as JPEG, the Space's ~2,600 images ~75 MB (PNG ~121 KB, ~340 MB) MIN_FREE = 12_000_000_000 # above the 8 GB the media loader keeps free RECHECK_BYTES, RECHECK_S = 64_000_000, 60 # each process re-reads the size and the free space this often _DIGEST = re.compile(r"^[0-9a-f]{24}$") _TMP = re.compile(r"^[0-9a-f]{64}\.b64\.\d+\.\d+\.tmp$") _MARK = "__check_image_cache__" logger = logging.getLogger("check_image_cache") _warned = set() def _warn_once(kind, message, *args): if kind not in _warned: _warned.add(kind) logger.warning(message, *args) def settings_digest(validation, module_file): """What, besides the stored image, shapes a prepared view: one directory per combination.""" try: from PIL import __version__ as pillow, features except ImportError: # no Pillow, no views to prepare (the tests' stand-in Validation) pillow = features = None config = validation._config dump = config.model_dump() if hasattr(config, "model_dump") else dict(vars(config)) codecs = {} for name in ("jpg", "zlib", "webp"): try: codecs[name] = features.version(name) except Exception: codecs[name] = None with open(module_file, "rb") as fh: module = hashlib.sha256(fh.read()).hexdigest() identity = { "config": dump, # all of it: a setting llm_client reads later can never share a stale directory "class": f"{type(validation).__module__}.{type(validation).__qualname__}", "module": module, "pillow": pillow, "codecs": codecs, "python": "%d.%d" % sys.version_info[:2], } return hashlib.sha256(json.dumps(identity, sort_keys=True, default=str).encode()).hexdigest()[:24] class Cache: """One Validation's files, ``//.b64``. Never raises: a failure is a miss.""" def __init__(self, directory): self.directory = directory self._lock = threading.Lock() self.counts = {"hit": 0, "written": 0, "not_kept": 0, "bypass": 0} self._used = self._free = None # bytes of views and free on the disk, as last read plus this process's writes self._since, self._read_at = 0, 0.0 def _count(self, outcome): with self._lock: self.counts[outcome] += 1 def _room(self, size): """True while the views stay under MAX_BYTES and the disk keeps MIN_FREE: both re-read from the disk every RECHECK_BYTES this process writes or RECHECK_S, so all processes together overshoot by little.""" with self._lock: now = time.monotonic() if self._used is None or self._since >= RECHECK_BYTES or now - self._read_at >= RECHECK_S: self._used, self._free = files(self.directory)[1], _free(self.directory) self._since, self._read_at = 0, now if self._used + size > MAX_BYTES or self._free - size < MIN_FREE: return False self._used += size self._free -= size self._since += size return True def view(self, validation, image): """``validation._encode_image(image)``, read from its file when there is one.""" path = self._path(validation, image) if path is None: self._count("bypass") return validation._encode_image(image) value = self._read(path) if value is not None: self._count("hit") return value value = validation._encode_image(image) # its errors are the check's, as without the cache self._count("written" if self._write(path, value) else "not_kept") return value def _path(self, validation, image): # Only stored base64 images: a str that is not one is a file path, whose content can change under its name. try: if isinstance(image, str) and validation._is_base64(image): return os.path.join(self.directory, hashlib.sha256(image.encode()).hexdigest() + ".b64") except Exception as e: _warn_once("key", "check image cache: no key for an image (%s); preparing it per check", e) return None def _read(self, path): try: with open(path, "rb") as fh: data = fh.read() except FileNotFoundError: return None except Exception as e: _warn_once("read", "check image cache: cannot read %s (%s); preparing views per check", path, e) return None head, _, body = data.partition(b"\n") try: size, crc = head.split(b" ") if int(size) == len(body) and int(crc, 16) == zlib.crc32(body): return body.decode("ascii") except (ValueError, UnicodeDecodeError): pass _warn_once("damaged", "check image cache: %s is damaged; preparing the view again", path) return None def _write(self, path, value): tmp = f"{path}.{os.getpid()}.{threading.get_ident()}.tmp" try: body = value.encode("ascii") if not self._room(len(body) + 32): _warn_once("full", "check image cache: full (%d MB of views, or under %d GB free on the disk); " "new views are prepared per check", MAX_BYTES // 1_000_000, MIN_FREE // 1_000_000_000) return False try: fh = open(tmp, "wb") except FileNotFoundError: os.makedirs(self.directory, exist_ok=True) fh = open(tmp, "wb") with fh: fh.write(b"%d %08x\n" % (len(body), zlib.crc32(body))) fh.write(body) os.replace(tmp, path) return True except Exception as e: _warn_once("write", "check image cache: cannot write in %s (%s); views are prepared per check", self.directory, e) try: os.unlink(tmp) except OSError: pass return False _caches = weakref.WeakKeyDictionary() _caches_lock = threading.Lock() def is_wrapped(cls): return getattr(getattr(cls, "_encode_reference_images", None), _MARK, None) is not None def cache_of(validation): """The Cache of a Validation whose class is wrapped, else None (also when its settings can't be read).""" mark = getattr(getattr(type(validation), "_encode_reference_images", None), _MARK, None) if mark is None: return None try: return _caches[validation] except KeyError: pass except TypeError: # not weak-referenceable: no cache return None root, module_file = mark try: cache = Cache(os.path.join(root, settings_digest(validation, module_file))) except Exception as e: _warn_once("settings", "check image cache off: cannot read the view settings (%s)", e) cache = None with _caches_lock: return _caches.setdefault(validation, cache) def patch(module, root): """Wrap ``module.Validation._encode_reference_images`` (llm_client's validation module); True when wrapped.""" cls = getattr(module, "Validation", None) original = getattr(cls, "_encode_reference_images", None) if original is None or not all(callable(getattr(cls, n, None)) for n in ("_encode_image", "_is_base64")): _warn_once("missing", "check image cache off: %s.Validation has no _encode_reference_images, " "_encode_image or _is_base64", getattr(module, "__name__", module)) return False if getattr(original, _MARK, None) is not None: return True @functools.wraps(original) def _encode_reference_images(self, reference_images): cache = cache_of(self) if reference_images else None if cache is None: return original(self, reference_images) return [cache.view(self, image) for image in reference_images] setattr(_encode_reference_images, _MARK, (root, getattr(module, "__file__", None))) cls._encode_reference_images = _encode_reference_images return True class _PatchOnImport: """A sys.meta_path finder: wraps llm_client's Validation right after its module has run, in the process that imports it. Every other import passes through untouched.""" def __init__(self, root): self.root = root def find_spec(self, name, path, target=None): if name != TARGET: return None try: from importlib.machinery import PathFinder, SourceFileLoader, SourcelessFileLoader spec = PathFinder.find_spec(name, path) if spec is None: return None # A file loader serves this one module, so its exec_module can be wrapped in place. if type(spec.loader) not in (SourceFileLoader, SourcelessFileLoader): _warn_once("hook", "check image cache off: %s is not loaded from a file", name) return None exec_module = spec.loader.exec_module root = self.root def exec_and_patch(module): exec_module(module) try: patch(module, root) except Exception as e: _warn_once("patch", "check image cache off: %s", e) spec.loader.exec_module = exec_and_patch return spec except Exception as e: _warn_once("hook", "check image cache off: %s", e) return None def install(environ=os.environ): """Arm the hook when CHECK_IMAGE_CACHE_DIR is set; with it unset or empty, nothing changes.""" root = (environ.get(ENV) or "").strip() if not root: return False module = sys.modules.get(TARGET) if module is not None: return patch(module, root) if not any(isinstance(f, _PatchOnImport) for f in sys.meta_path): sys.meta_path.insert(0, _PatchOnImport(root)) return True async def fill(validation, loader, threads=4, page=500, batch=25): """Prepare every image of the loader's index through ``validation._encode_reference_images``. The ids come from a scroll of the index, the images from ``loader.load_images`` (what a check loads), and up to ``threads`` views are prepared at once in threads. Returns {"images", "empty", "failed"}. """ import asyncio from concurrent.futures import ThreadPoolExecutor counts = {"images": 0, "empty": 0, "failed": 0} loop = asyncio.get_running_loop() slots = asyncio.Semaphore(threads * 2) pending = set() def done(task): # on the loop's thread slots.release() pending.discard(task) if task.cancelled() or task.exception() is not None: counts["failed"] += 1 with ThreadPoolExecutor(threads, thread_name_prefix="check-image-cache") as pool: client, index = loader.client, loader.image_cache_index resp = await client.search(index=index, body={"query": {"match_all": {}}, "_source": False, "sort": ["_doc"]}, size=page, scroll="5m") try: while resp["hits"]["hits"]: ids = [hit["_id"] for hit in resp["hits"]["hits"]] for i in range(0, len(ids), batch): images = await loader.load_images(ids[i:i + batch]) for image_id in ids[i:i + batch]: image = images.get(image_id) if not isinstance(image, str) or not image: counts["empty"] += 1 continue counts["images"] += 1 await slots.acquire() task = loop.run_in_executor(pool, validation._encode_reference_images, [image]) pending.add(task) task.add_done_callback(done) resp = await client.scroll(scroll_id=resp["_scroll_id"], scroll="5m") finally: if pending: await asyncio.gather(*pending, return_exceptions=True) try: await client.clear_scroll(scroll_id=resp["_scroll_id"]) except Exception: pass return counts def files(directory): """(count, bytes) of the views in a settings directory.""" n = size = 0 try: with os.scandir(directory) as it: for entry in it: if entry.name.endswith(".b64"): try: size += entry.stat().st_size n += 1 except OSError: # replaced or removed meanwhile pass except OSError: pass return n, size def _free(path): """Free bytes on the disk that holds ``path`` (its nearest existing parent); 0 when unreadable: no writes.""" try: while not os.path.exists(path) and os.path.dirname(path) != path: path = os.path.dirname(path) return shutil.disk_usage(path).free except OSError: return 0 def sweep(directory, older_than=600): """What an earlier start left in the cache's root: other settings' directories, and temp files of writers that died (older than ``older_than`` s). Only names this module makes are touched. Returns the number removed.""" root, removed = os.path.dirname(directory), 0 try: with os.scandir(root) as it: for entry in it: if _DIGEST.match(entry.name) and entry.path != directory and entry.is_dir(follow_symlinks=False): shutil.rmtree(entry.path, ignore_errors=True) removed += 1 with os.scandir(directory) as it: for entry in it: if _TMP.match(entry.name) and time.time() - entry.stat().st_mtime > older_than: os.unlink(entry.path) removed += 1 except OSError: pass return removed def self_test(): """The image build's check (Dockerfile.hf-space-gpu): the .pth loaded this module and it wrapped the vendored llm_client, views read back from the cache are byte-identical to llm_client's own and in the format the environment names (USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT, unset: PNG), and the warm-up's imports exist. Raises on any difference. Writes only in CHECK_IMAGE_CACHE_DIR, which the build then removes.""" import base64 import io from PIL import Image from deepsearch_api.routers_v2.dependencies import build_image_loader # noqa: F401 (the warm-up's imports) from deepsearch_api.search_backend.main import SearchSettings # noqa: F401 from deepsearch_api.validation.config import ValidationSettings from llm_client.validation.validation import Validation assert "check_image_cache" in sys.modules and is_wrapped(Validation), "the hook did not wrap llm_client" global MIN_FREE MIN_FREE = 0 # the image builder's disk is not the Space's def b64(image, fmt, **kw): buf = io.BytesIO() image.save(buf, fmt, **kw) return base64.b64encode(buf.getvalue()).decode() noise = Image.effect_noise((1024, 1024), 40) rgb = Image.merge("RGB", (noise, Image.linear_gradient("L").resize((1024, 1024)), noise.rotate(90))) rgba = rgb.resize((900, 600)) rgba.putalpha(Image.linear_gradient("L").resize((900, 600))) images = [b64(rgb, "JPEG", quality=90), b64(rgba, "PNG"), b64(rgb.resize((300, 200)), "JPEG", quality=90)] config = ValidationSettings(image_max_px=448).to_validation_config() plain = [Validation(config=config)._encode_image(image) for image in images] # Wheels that dropped the format setting would pass the build's JPEG run with PNG views. wanted = os.environ.get("USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT") or "png" magic = {"png": "iVBORw0KGgo", "jpeg": "/9j/"}[wanted] assert all(view.startswith(magic) for view in plain), f"the views are not {wanted}" miss = Validation(config=config)._encode_reference_images(images) reader = Validation(config=config) hit = reader._encode_reference_images(images) assert miss == plain and hit == plain, "a cached view differs from llm_client's own" assert cache_of(reader).counts["hit"] == len(images), "the views were not read back from the cache" return len(images) def main(argv=None): """``python -m check_image_cache [--threads N]``: fill the cache from the image cache index. Always exits 0 (``--self-test`` exits 1 when the check fails).""" import argparse import asyncio import resource parser = argparse.ArgumentParser(prog="check_image_cache") parser.add_argument("--threads", type=int, default=4) parser.add_argument("--self-test", action="store_true") args = parser.parse_args(argv) say = functools.partial(print, "check-image-cache:", flush=True) if not (os.environ.get(ENV) or "").strip(): say(f"{ENV} is not set: nothing to do") return 1 if args.self_test else 0 if args.self_test: say(f"self-test passed: {self_test()} views read back byte-identical") return 0 started, cpu = time.monotonic(), time.process_time() try: from deepsearch_api.validation.config import ValidationSettings settings = ValidationSettings() if not settings.enabled: say("vision validation is off: nothing to do") return 0 from llm_client.validation.validation import Validation validation = Validation(config=settings.to_validation_config()) cache = cache_of(validation) if cache is None: say("llm_client's Validation is not wrapped (see the warning above): nothing to do") return 0 swept = sweep(cache.directory) from deepsearch_api.routers_v2.dependencies import build_image_loader from deepsearch_api.search_backend.main import SearchSettings loader = build_image_loader(SearchSettings()) async def run(): try: return await fill(validation, loader, threads=max(1, args.threads)) finally: await loader.client.close() counts = asyncio.run(run()) except Exception as e: say(f"stopped ({type(e).__name__}: {e}); checks prepare the rest as they go") return 0 n, size = files(cache.directory) c = cache.counts say(f"{counts['images']} images in {time.monotonic() - started:.1f} s, {time.process_time() - cpu:.1f} CPU-s: " f"{c['written']} written, {c['hit']} already there, {c['not_kept']} not kept, {c['bypass']} not cacheable, " f"{counts['failed']} failed, {counts['empty']} missing or not loaded; {n} files, {size / 1e6:.0f} MB in " f"{cache.directory} ({swept} old entries removed); " f"peak RSS {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024:.0f} MB") return 0 if __name__ == "__main__": import check_image_cache # the instance the .pth imported: its hook wrapped Validation sys.exit(check_image_cache.main()) else: try: install() except Exception as e: # never break a python start _warn_once("install", "check image cache off: %s", e)