"""Bind AoTI constants that `torch.export` lifted anonymously. Problem ------- `spaces.zero.torch.aoti.LazyAOTIModel` binds a compiled package's constants **by name**:: constant_fqns = compiled_model.get_constant_fqns() constant_map = {name: tensor for name, tensor in weights.items() if name in constant_fqns} compiled_model.load_constants(constant_map, check_full_update=check_full_update, user_managed=True) `torch.export` only gives a lifted tensor a real FQN when it was a registered parameter or buffer. Anything reached through a plain python attribute is classified `CONSTANT_TENSOR` and the compiled artifact names it `_tensor_constant` — a name that can never appear in `state_dict()`. The intersection above is then empty, the dict comprehension silently drops every weight, and the compiled model runs against constants nobody ever set: a SIGSEGV rather than an error. This module fixes both halves: * `write_constant_aliases(...)` — compile side. Records the exact `_tensor_constant -> real.dotted.fqn` mapping, which the `ExportedProgram` knows even when the compiled package does not, into a `constant_aliases.json` sidecar next to `package.pt2`. * `apply_spaces_constant_binding_patch()` — load side. Monkeypatches `LazyAOTIModel.__call__` so it (1) uses that sidecar when present, (2) otherwise falls back to matching anonymous constants against the leftover `state_dict()` entries by dtype+shape read out of the package's own `wrapper.cpp`, and (3) **raises** if the binding is not total instead of segfaulting later. The load-side patch alone is enough to turn the crash into a clear diagnostic; with the sidecar it also makes the package work. """ from __future__ import annotations import io import json import re import zipfile from pathlib import Path import torch ALIASES_FILENAME = "constant_aliases.json" _DTYPES = { "float32": torch.float32, "float64": torch.float64, "float16": torch.float16, "bfloat16": torch.bfloat16, "float8_e4m3fn": torch.float8_e4m3fn, "float8_e5m2": torch.float8_e5m2, "float8_e4m3fnuz": torch.float8_e4m3fnuz, "float8_e5m2fnuz": torch.float8_e5m2fnuz, "int8": torch.int8, "uint8": torch.uint8, "int16": torch.int16, "int32": torch.int32, "int64": torch.int64, "bool": torch.bool, } # --------------------------------------------------------------------------- compile side def register_loose_tensors(module: torch.nn.Module, prefix: str = "") -> list[str]: """Re-register plain tensor attributes as buffers so `torch.export` gives them real FQNs. Model-agnostic and numerics-preserving: it changes how a tensor is *registered*, never the tensor and never the forward. Run it on the shallow clone right after `unwrap_tensor_subclass_parameters`, immediately before `torch.export.export`. Returns the names it re-registered, which is empty for a module that was already well-formed. """ registered = [] for name, value in list(vars(module).items()): if not isinstance(value, torch.Tensor) or name.startswith("_"): continue if name in module._parameters or name in module._buffers: continue object.__delattr__(module, name) module.register_buffer(name, value, persistent=True) registered.append(f"{prefix}{name}") for child_name, child in module.named_children(): registered += register_loose_tensors(child, f"{prefix}{child_name}.") return registered def constant_aliases_from_exported_program(exported_program) -> dict[str, str]: """`{'_tensor_constant': ''}` for every anonymously lifted constant. AOT Inductor numbers its `_tensor_constant` slots in the order the `CONSTANT_TENSOR` inputs appear in the export graph signature, and the signature still carries each one's real FQN. """ targets = [ spec.target for spec in exported_program.graph_signature.input_specs if spec.kind.name == "CONSTANT_TENSOR" ] return {f"_tensor_constant{index}": target for index, target in enumerate(targets)} def write_constant_aliases(package_dir, exported_program, submodule: str | None = None) -> Path | None: """Drop the alias sidecar next to the `package.pt2` `aoti_compile_and_save` just wrote.""" aliases = constant_aliases_from_exported_program(exported_program) if not aliases: return None subdir = Path(package_dir) / ("submodules/" + submodule if submodule else "root") path = subdir / ALIASES_FILENAME path.write_text(json.dumps(aliases, indent=2)) return path # --------------------------------------------------------------------------- load side def _package_constants_info(archive_file) -> list[dict]: """Read `constants_info_` (dtype, shape, in slot order) out of a `.pt2`'s wrapper source.""" if isinstance(archive_file, (str, Path)): handle: object = str(archive_file) else: position = archive_file.tell() archive_file.seek(0) handle = io.BytesIO(archive_file.read()) archive_file.seek(position) with zipfile.ZipFile(handle) as archive: # pyright: ignore[reportArgumentType] names = [n for n in archive.namelist() if n.endswith(".wrapper.cpp")] if not names: return [] source = archive.read(names[0]).decode() info: dict[int, dict] = {} for match in re.finditer(r"constants_info_\[(\d+)\]\.(\w+) = ([^;]+);", source): index, field, value = int(match.group(1)), match.group(2), match.group(3).strip() entry = info.setdefault(index, {}) if field == "dtype": entry["dtype"] = _DTYPES.get(value.replace("cached_torch_dtype_", "")) elif field == "shape": entry["shape"] = tuple(int(x) for x in re.findall(r"-?\d+", value)) elif field in ("name", "original_fqn"): entry[field] = value.strip('"') return [info[index] for index in sorted(info)] def resolve_constant_map( archive_file, constant_fqns, weights: dict[str, torch.Tensor], aliases=None, allow_shape_fallback: bool = False, ): """Map every compiled constant FQN onto one of `weights`, or explain why it cannot.""" constant_map = {name: weights[name] for name in constant_fqns if name in weights} missing = [name for name in constant_fqns if name not in constant_map] if not missing: return constant_map, [] # 1. the exact mapping, if the compile side recorded one aliases = aliases or {} for name in list(missing): target = aliases.get(name) if target is not None and target in weights: constant_map[name] = weights[target] missing.remove(name) if not missing or not allow_shape_fallback: return constant_map, missing # 2. otherwise match by dtype+shape against the state_dict entries nobody claimed, preserving # each side's own order inside a (dtype, shape) group. `get_constant_fqns()` returns the # slots in *lexicographic* order (`_tensor_constant10` before `_tensor_constant2`), so the # package's own `constants_info_` index is the only correct order to walk them in. info = _package_constants_info(archive_file) by_name = {entry.get("name"): entry for entry in info} slot_index = {entry.get("name"): index for index, entry in enumerate(info)} taken = {id(tensor) for tensor in constant_map.values()} buckets: dict[tuple, list[torch.Tensor]] = {} for tensor in weights.values(): if id(tensor) not in taken: buckets.setdefault((tensor.dtype, tuple(tensor.shape)), []).append(tensor) for name in sorted(list(missing), key=lambda n: slot_index.get(n, 1 << 30)): entry = by_name.get(name) if entry is None or entry.get("dtype") is None: continue bucket = buckets.get((entry["dtype"], entry["shape"])) if bucket: constant_map[name] = bucket.pop(0) missing.remove(name) return constant_map, missing def apply_spaces_constant_binding_patch(strict: bool = True, allow_shape_fallback: bool = False): """Make `spaces`' AoTI loader bind anonymous constants, and fail loudly if it still cannot. Call once, before any `spaces.aoti_*` loading. Idempotent. """ from spaces.zero.torch import aoti as spaces_aoti if getattr(spaces_aoti.LazyAOTIModel, "_constant_binding_patched", False): return original_call = spaces_aoti.LazyAOTIModel.__call__ def patched_call(self, weights, check_full_update, *args, **kwargs): compiled_model = self.compiled_model.get() if compiled_model is None: with spaces_aoti._register_aoti_cleanup(): compiled_model = torch._inductor.aoti_load_package(self.archive_file) self.compiled_model.set(compiled_model) loaded = self.loaded_weights.get() if loaded is None or loaded is not weights: fqns = compiled_model.get_constant_fqns() aliases = getattr(self, "_constant_aliases", None) if aliases is None: aliases = {} if isinstance(self.archive_file, (str, Path)): sidecar = Path(self.archive_file).with_name(ALIASES_FILENAME) if sidecar.is_file(): aliases = json.loads(sidecar.read_text()) self._constant_aliases = aliases constant_map, missing = resolve_constant_map( self.archive_file, fqns, weights, aliases, allow_shape_fallback ) if missing and strict: raise RuntimeError( f"{len(missing)} of {len(fqns)} AoTI constants could not be bound to the module's " f"state_dict: {missing[:8]}. Anonymous `_tensor_constant*` names mean the export saw " f"plain tensor attributes rather than registered parameters or buffers. Register them " f"(or write a {ALIASES_FILENAME} sidecar at compile time) — binding them partially " f"would leave the compiled model dereferencing unset constants." ) compiled_model.load_constants( constant_map, check_full_update=check_full_update and not missing, user_managed=True ) self.loaded_weights.set(weights) return compiled_model(*args, **kwargs) spaces_aoti.LazyAOTIModel.__call__ = patched_call spaces_aoti.LazyAOTIModel._constant_binding_patched = True spaces_aoti.LazyAOTIModel._original_call = original_call