CoolFace
Apppublic

multimodalart/minimax-h3-reference

sourceHugging Faceupdated 1mo agoView on Hugging Face
91likes
spaces_constant_binding_patch.py203 linesDownload Raw Back to root
1"""Bind AoTI constants that `torch.export` lifted anonymously.2 3`spaces.zero.torch.aoti.LazyAOTIModel` binds a package's constants by intersecting the module's `state_dict()` with4`compiled_model.get_constant_fqns()`, and keeps whatever it cannot match. `torch.export` only gives a lifted tensor a5real FQN when it was a registered parameter or buffer; anything else is named `_tensor_constant<N>`, which no6`state_dict()` can contain, so the compiled model runs against constants nobody set — a SIGSEGV rather than an error.7 8`write_constant_aliases` records the real names on the compile side; `apply_spaces_constant_binding_patch` uses that9sidecar on the load side, falls back to matching by dtype+shape, and raises if the binding is still not total.10"""11 12from __future__ import annotations13 14import io15import json16import re17import zipfile18from pathlib import Path19 20import torch21 22ALIASES_FILENAME = "constant_aliases.json"23 24_DTYPES = {25    "float32": torch.float32, "float64": torch.float64, "float16": torch.float16,26    "bfloat16": torch.bfloat16, "float8_e4m3fn": torch.float8_e4m3fn,27    "float8_e5m2": torch.float8_e5m2, "float8_e4m3fnuz": torch.float8_e4m3fnuz,28    "float8_e5m2fnuz": torch.float8_e5m2fnuz, "int8": torch.int8, "uint8": torch.uint8,29    "int16": torch.int16, "int32": torch.int32, "int64": torch.int64, "bool": torch.bool,30}31 32 33# --------------------------------------------------------------------------- compile side34 35 36def register_loose_tensors(module: torch.nn.Module, prefix: str = "") -> list[str]:37    """Re-register plain tensor attributes as buffers so `torch.export` gives them real FQNs.38 39    Run on the shallow clone, right before `torch.export.export`. Returns the names it re-registered.40    """41    registered = []42    for name, value in list(vars(module).items()):43        if not isinstance(value, torch.Tensor) or name.startswith("_"):44            continue45        if name in module._parameters or name in module._buffers:46            continue47        object.__delattr__(module, name)48        module.register_buffer(name, value, persistent=True)49        registered.append(f"{prefix}{name}")50    for child_name, child in module.named_children():51        registered += register_loose_tensors(child, f"{prefix}{child_name}.")52    return registered53 54 55def constant_aliases_from_exported_program(exported_program) -> dict[str, str]:56    """`{'_tensor_constant<N>': '<real dotted fqn>'}` for every anonymously lifted constant.57 58    AOT Inductor numbers its slots in the order the `CONSTANT_TENSOR` inputs appear in the graph signature, which59    still carries each one's real FQN.60    """61    targets = [62        spec.target63        for spec in exported_program.graph_signature.input_specs64        if spec.kind.name == "CONSTANT_TENSOR"65    ]66    return {f"_tensor_constant{index}": target for index, target in enumerate(targets)}67 68 69def write_constant_aliases(package_dir, exported_program, submodule: str | None = None) -> Path | None:70    """Drop the alias sidecar next to the `package.pt2` `aoti_compile_and_save` just wrote."""71    aliases = constant_aliases_from_exported_program(exported_program)72    if not aliases:73        return None74    subdir = Path(package_dir) / ("submodules/" + submodule if submodule else "root")75    path = subdir / ALIASES_FILENAME76    path.write_text(json.dumps(aliases, indent=2))77    return path78 79 80# --------------------------------------------------------------------------- load side81 82 83def _package_constants_info(archive_file) -> list[dict]:84    """Read `constants_info_` (dtype, shape, in slot order) out of a `.pt2`'s wrapper source."""85    if isinstance(archive_file, (str, Path)):86        handle: object = str(archive_file)87    else:88        position = archive_file.tell()89        archive_file.seek(0)90        handle = io.BytesIO(archive_file.read())91        archive_file.seek(position)92    with zipfile.ZipFile(handle) as archive:  # pyright: ignore[reportArgumentType]93        names = [n for n in archive.namelist() if n.endswith(".wrapper.cpp")]94        if not names:95            return []96        source = archive.read(names[0]).decode()97    info: dict[int, dict] = {}98    for match in re.finditer(r"constants_info_\[(\d+)\]\.(\w+) = ([^;]+);", source):99        index, field, value = int(match.group(1)), match.group(2), match.group(3).strip()100        entry = info.setdefault(index, {})101        if field == "dtype":102            entry["dtype"] = _DTYPES.get(value.replace("cached_torch_dtype_", ""))103        elif field == "shape":104            entry["shape"] = tuple(int(x) for x in re.findall(r"-?\d+", value))105        elif field in ("name", "original_fqn"):106            entry[field] = value.strip('"')107    return [info[index] for index in sorted(info)]108 109 110def resolve_constant_map(111    archive_file,112    constant_fqns,113    weights: dict[str, torch.Tensor],114    aliases=None,115    allow_shape_fallback: bool = False,116):117    """Map every compiled constant FQN onto one of `weights`, or report what is left over."""118    constant_map = {name: weights[name] for name in constant_fqns if name in weights}119    missing = [name for name in constant_fqns if name not in constant_map]120    if not missing:121        return constant_map, []122 123    aliases = aliases or {}124    for name in list(missing):125        target = aliases.get(name)126        if target is not None and target in weights:127            constant_map[name] = weights[target]128            missing.remove(name)129    if not missing or not allow_shape_fallback:130        return constant_map, missing131 132    # Match by dtype+shape against the unclaimed `state_dict()` entries, preserving each side's own order inside a133    # (dtype, shape) group. `get_constant_fqns()` returns slots lexicographically (`_tensor_constant10` before134    # `_tensor_constant2`), so the package's own `constants_info_` index is the only correct order to walk them in.135    info = _package_constants_info(archive_file)136    by_name = {entry.get("name"): entry for entry in info}137    slot_index = {entry.get("name"): index for index, entry in enumerate(info)}138    taken = {id(tensor) for tensor in constant_map.values()}139    buckets: dict[tuple, list[torch.Tensor]] = {}140    for tensor in weights.values():141        if id(tensor) not in taken:142            buckets.setdefault((tensor.dtype, tuple(tensor.shape)), []).append(tensor)143    for name in sorted(list(missing), key=lambda n: slot_index.get(n, 1 << 30)):144        entry = by_name.get(name)145        if entry is None or entry.get("dtype") is None:146            continue147        bucket = buckets.get((entry["dtype"], entry["shape"]))148        if bucket:149            constant_map[name] = bucket.pop(0)150            missing.remove(name)151    return constant_map, missing152 153 154def apply_spaces_constant_binding_patch(strict: bool = True, allow_shape_fallback: bool = False):155    """Make `spaces`' AoTI loader bind anonymous constants, and fail loudly if it still cannot.156 157    Call once, before any `spaces.aoti_*` loading. Idempotent.158    """159    from spaces.zero.torch import aoti as spaces_aoti160 161    if getattr(spaces_aoti.LazyAOTIModel, "_constant_binding_patched", False):162        return163 164    original_call = spaces_aoti.LazyAOTIModel.__call__165 166    def patched_call(self, weights, check_full_update, *args, **kwargs):167        compiled_model = self.compiled_model.get()168        if compiled_model is None:169            with spaces_aoti._register_aoti_cleanup():170                compiled_model = torch._inductor.aoti_load_package(self.archive_file)171            self.compiled_model.set(compiled_model)172        loaded = self.loaded_weights.get()173        if loaded is None or loaded is not weights:174            fqns = compiled_model.get_constant_fqns()175            aliases = getattr(self, "_constant_aliases", None)176            if aliases is None:177                aliases = {}178                if isinstance(self.archive_file, (str, Path)):179                    sidecar = Path(self.archive_file).with_name(ALIASES_FILENAME)180                    if sidecar.is_file():181                        aliases = json.loads(sidecar.read_text())182                self._constant_aliases = aliases183            constant_map, missing = resolve_constant_map(184                self.archive_file, fqns, weights, aliases, allow_shape_fallback185            )186            if missing and strict:187                raise RuntimeError(188                    f"{len(missing)} of {len(fqns)} AoTI constants could not be bound to the module's "189                    f"state_dict: {missing[:8]}. Anonymous `_tensor_constant*` names mean the export saw "190                    f"plain tensor attributes rather than registered parameters or buffers. Register them "191                    f"(or write a {ALIASES_FILENAME} sidecar at compile time) — binding them partially "192                    f"would leave the compiled model dereferencing unset constants."193                )194            compiled_model.load_constants(195                constant_map, check_full_update=check_full_update and not missing, user_managed=True196            )197            self.loaded_weights.set(weights)198        return compiled_model(*args, **kwargs)199 200    spaces_aoti.LazyAOTIModel.__call__ = patched_call201    spaces_aoti.LazyAOTIModel._constant_binding_patched = True202    spaces_aoti.LazyAOTIModel._original_call = original_call203