Spaces:
Running on Zero
Running on Zero
Wire the AoTI load path (env-gated) and the sub-768p canvases
Browse files- h3_aoti.py +431 -0
h3_aoti.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ZeroGPU AoTI for MiniMax-H3: compile the repeated transformer block once, reuse the package forever.
|
| 2 |
+
|
| 3 |
+
Shared byte-identically by every MiniMax-H3 Space. A Space only ever calls `maybe_load()`; the compile path runs from
|
| 4 |
+
the debug Space's "Compile (AoTI)" tab, or off-Space from `job_bf16_aoti.py` on an `rtx-pro-6000` Job, and pushes its
|
| 5 |
+
artifacts to `diffusers-internal-dev/minimax-h3-aoti` under `<width>/torch<X.Y>/sm<cc>/<shape>`.
|
| 6 |
+
|
| 7 |
+
What is measured, so nobody has to guess whether this is worth turning on. Unquantized bfloat16, 124 frames,
|
| 8 |
+
everything resident, one dynamic-sequence package serving every row — on an RTX PRO 6000 Blackwell, torch 2.11,
|
| 9 |
+
cuDNN attention:
|
| 10 |
+
|
| 11 |
+
canvas (HxW) eager s/step AoTI s/step saved faster
|
| 12 |
+
768x1344 10.20 9.73 0.47 s +4.6%
|
| 13 |
+
704x1280 8.59 7.87 0.72 s +8.4%
|
| 14 |
+
640x1152 6.46 5.88 0.59 s +9.1%
|
| 15 |
+
576x1024 4.74 4.24 0.50 s +10.5%
|
| 16 |
+
544x960 4.02 3.58 0.44 s +11.0%
|
| 17 |
+
|
| 18 |
+
Read the *absolute* column: AoTI removes a near-constant ~0.5 s/step no matter how big the canvas is. That is exactly
|
| 19 |
+
the shape of what it can remove — 50 blocks' worth of kernel-launch overhead and the norm / rotary / AdaLN-gather
|
| 20 |
+
epilogues around the matmuls. It cannot touch the matmuls themselves, and at S = 37726 one block is ~70 TFLOP of GEMM
|
| 21 |
+
and attention, so the released 768x1344 canvas is compute bound and only 4.6% comes back. The smaller the default
|
| 22 |
+
canvas gets, the better this pays.
|
| 23 |
+
|
| 24 |
+
The trap that cost a day, recorded here because the symptom is a segfault with no message: a **shallow clone exported
|
| 25 |
+
in torch.export's default non-strict mode duplicates every weight** — once as a named `PARAMETER` and once as an
|
| 26 |
+
anonymous `lifted_tensor_<N>` `CONSTANT_TENSOR` aliasing the same storage — and `LazyAOTIModel` binds constants by
|
| 27 |
+
name, so the anonymous half binds to nothing and the compiled kernel reads pointers nobody set. It is neither
|
| 28 |
+
accelerate's offload hooks nor torchao's tensor subclasses, which were both blamed first; it reproduces in plain
|
| 29 |
+
bfloat16 with no subclass anywhere, and it goes away with `strict=True`. See `export_block`.
|
| 30 |
+
|
| 31 |
+
Why block level rather than the whole transformer: `MiniMaxH3Transformer3DModel.forward` decides whether the packed
|
| 32 |
+
sequence needs a padding attention mask with `bool(is_pad.any())`, a data-dependent branch `torch.export` cannot
|
| 33 |
+
trace. One `MiniMaxH3TransformerBlock` is where all the time goes anyway (50 of them per step), and the sequence
|
| 34 |
+
length is the only thing that changes between requests, which a single dynamic dimension covers. The block's fifth
|
| 35 |
+
argument, `attention_mask`, is always `None` in practice — `packing.py` never emits a padding row, so `token_tags` is
|
| 36 |
+
never negative — which is what makes one static signature enough.
|
| 37 |
+
|
| 38 |
+
What `spaces` 0.51.1 actually provides (checked against the installed package, not the klein-era blog post):
|
| 39 |
+
|
| 40 |
+
spaces.aoti_capture(module) context manager, grabs the args of the next call and aborts it
|
| 41 |
+
spaces.aoti_compile(exported_program, configs) in-process compile, returns a ZeroGPUCompiledModel
|
| 42 |
+
spaces.aoti_compile_and_save(dir, ep, configs, submodule=...)
|
| 43 |
+
compile and write `<dir>/submodules/<submodule>/package.pt2`
|
| 44 |
+
spaces.aoti_apply(compiled, module) in-process apply
|
| 45 |
+
spaces.aoti_patch(module, LazyAOTIModel) apply a package to one module, weights stay live
|
| 46 |
+
spaces.aoti_load_from_package_dir(module, dir) walk `<dir>/{root,submodules/*}` and patch, ModuleList aware
|
| 47 |
+
spaces.aoti_load(module, repo_id, ...) the convenience wrapper — NOT usable here: it hardcodes
|
| 48 |
+
`snapshot_download(allow_patterns="package/*")` on a *model*
|
| 49 |
+
repo, and these artifacts are keyed by quant/torch/arch under a
|
| 50 |
+
dataset, so the download is done here and only the loader
|
| 51 |
+
(`aoti_load_from_package_dir`) is reused.
|
| 52 |
+
spaces.aoti_blocks_load(module, repo_id, variant) the `_repeated_blocks` convenience — same repo-layout mismatch.
|
| 53 |
+
|
| 54 |
+
Weights are *not* baked into the package: `aoti_patch` binds the block's live `state_dict()`, so one package serves all
|
| 55 |
+
50 blocks, and the quantized weights it reads are whatever the block holds. Which is also why quantization has to
|
| 56 |
+
happen *before* the export — the same ordering constraint as fusing a LoRA before AoTI.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
from __future__ import annotations
|
| 60 |
+
|
| 61 |
+
import os
|
| 62 |
+
from pathlib import Path
|
| 63 |
+
|
| 64 |
+
AOTI = os.environ.get("H3_AOTI", "0") == "1"
|
| 65 |
+
AOTI_REPO = os.environ.get("H3_AOTI_REPO", "diffusers-internal-dev/minimax-h3-aoti")
|
| 66 |
+
AOTI_REPO_TYPE = os.environ.get("H3_AOTI_REPO_TYPE", "dataset")
|
| 67 |
+
# `dynamic` is the one package that serves every canvas, duration *and prompt*, and for bfloat16 it is what gets built:
|
| 68 |
+
# a dynamic sequence dimension exports and compiles cleanly (measured on an rtx-pro-6000 Job, torch 2.11). It has to be
|
| 69 |
+
# dynamic to be useful at all — `build_packed_sequence` pads nothing, so
|
| 70 |
+
# `S = num_text_tokens + condition_rows + audio_rows + video_rows` moves with the *prompt* as well as the canvas, and a
|
| 71 |
+
# static package would only ever serve the one prompt length it was captured from.
|
| 72 |
+
#
|
| 73 |
+
# A `HxWxF` value instead pins the artifact to one static shape. That is the fallback for a width whose dynamic export
|
| 74 |
+
# is refused, which is what the NVFP4 attempt hit: export rejected the dimension and offered only the affine
|
| 75 |
+
# `S = 128 * k - 34` it had derived from that one capture — an offset that is a property of one canvas *and* one prompt
|
| 76 |
+
# rather than of the model, so a package built that way serves almost nothing. The 128 is the alignment torchao's
|
| 77 |
+
# scaled matmuls want, though that has not been re-verified since the bfloat16 path was proven, and a static package is
|
| 78 |
+
# only worth building for a width that has been shown to need one.
|
| 79 |
+
AOTI_SHAPE = os.environ.get("H3_AOTI_SHAPE", "dynamic")
|
| 80 |
+
AOTI_DURATION = int(os.environ.get("H3_AOTI_DURATION", "1500"))
|
| 81 |
+
|
| 82 |
+
# The 50-deep stack that is the whole cost of a step. `MiniMaxH3TokenRefinerBlock` is also in `_repeated_blocks` but
|
| 83 |
+
# runs a handful of text rows a couple of times per step, so it is left eager.
|
| 84 |
+
BLOCK_CONTAINER = "transformer_blocks"
|
| 85 |
+
|
| 86 |
+
# Height of the AdaLN table baked into the package. `temb` is `(num_distinct_timesteps, time_embed_dim)` and the block
|
| 87 |
+
# gathers from a `3 * num_distinct_timesteps` table with `adaln_indices = timestep_indices * 3 + token_tag`, so the
|
| 88 |
+
# table's height is part of the compiled shape. It is not constant at runtime: at step 0 the video and audio streams
|
| 89 |
+
# share a noise level and `temb` has a single row, and from step 1 their sigma schedules diverge and it grows one.
|
| 90 |
+
# Exporting whatever the first call happened to show bakes in a 3-row table and the later steps then walk off it:
|
| 91 |
+
#
|
| 92 |
+
# Assertion `index out of bounds: 0 <= tmp22 < 3` failed
|
| 93 |
+
#
|
| 94 |
+
# A dynamic dimension is the wrong tool — `torch.export` specializes size-1 dimensions unconditionally, so a `Dim`
|
| 95 |
+
# taken from a 2-row capture carries a `>= 2` guard that step 0 violates. Instead `temb` is padded to a fixed height
|
| 96 |
+
# on both sides of the compile. Rows past the live ones are never gathered, so the output is unchanged, and the shape
|
| 97 |
+
# becomes a constant. Must match the `H3_AOTI_TEMB_ROWS` the package was compiled with.
|
| 98 |
+
#
|
| 99 |
+
# 4 is what the published bfloat16 packages were built with, and the padding is *validated* rather than assumed: the
|
| 100 |
+
# build job replays a real 1-row (step 0) call and a real 2-row (step 1) call through the compiled block and diffs both
|
| 101 |
+
# against eager. Two streams at two noise levels is the realistic maximum, so 4 is loose on purpose, and the cost is
|
| 102 |
+
# one slightly taller AdaLN projection per block against the block's own 70 TFLOP.
|
| 103 |
+
TEMB_ROWS = int(os.environ.get("H3_AOTI_TEMB_ROWS", "4"))
|
| 104 |
+
|
| 105 |
+
_LOADED: set[int] = set()
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def pad_temb(temb, rows: int = TEMB_ROWS):
|
| 109 |
+
"""Grow `temb` to exactly `rows` timestep rows by repeating its last one."""
|
| 110 |
+
present = temb.shape[0]
|
| 111 |
+
if present == rows:
|
| 112 |
+
return temb
|
| 113 |
+
if present > rows:
|
| 114 |
+
raise RuntimeError(
|
| 115 |
+
f"{present} distinct timesteps, but this AoTI package holds at most {rows}. "
|
| 116 |
+
f"Recompile with H3_AOTI_TEMB_ROWS>={present}."
|
| 117 |
+
)
|
| 118 |
+
import torch
|
| 119 |
+
|
| 120 |
+
return torch.cat([temb, temb[-1:].expand(rows - present, *temb.shape[1:])], dim=0)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def width() -> str:
|
| 124 |
+
"""Which transformer these artifacts belong to: `bf16`, `fp8`, `nvfp4`, ...
|
| 125 |
+
|
| 126 |
+
`H3_WIDTH` wins, so a Space that has no `h3_core` — the unquantized split deployment is two standalone Spaces —
|
| 127 |
+
can use this module by setting one variable. Otherwise it comes from `h3_core`, which derives it from `H3_QUANT`
|
| 128 |
+
or from the pre-quantized repository's suffix.
|
| 129 |
+
"""
|
| 130 |
+
if explicit := os.environ.get("H3_WIDTH"):
|
| 131 |
+
return explicit.lower()
|
| 132 |
+
try:
|
| 133 |
+
import h3_core
|
| 134 |
+
|
| 135 |
+
return h3_core.WIDTH
|
| 136 |
+
except Exception:
|
| 137 |
+
return "bf16"
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def artifact_key() -> str:
|
| 141 |
+
"""`<width>/torch<X.Y>/sm<cc>/<shape>` — an AoTI package is valid for exactly one of each."""
|
| 142 |
+
import torch
|
| 143 |
+
|
| 144 |
+
torch_version = ".".join(torch.__version__.split(".")[:2])
|
| 145 |
+
major, minor = torch.cuda.get_device_capability()
|
| 146 |
+
return f"{width()}/torch{torch_version}/sm{major}{minor}/{AOTI_SHAPE}"
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def status() -> str:
|
| 150 |
+
return (
|
| 151 |
+
f"AoTI **on** · `{AOTI_REPO}` ({AOTI_REPO_TYPE}) · shape `{AOTI_SHAPE}`"
|
| 152 |
+
if AOTI
|
| 153 |
+
else "AoTI **off** (`H3_AOTI=1` to load compiled blocks)"
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def patch_blocks(transformer, package_dir) -> None:
|
| 158 |
+
"""Point all 50 blocks at the one compiled package, binding each block's own weights on its first call.
|
| 159 |
+
|
| 160 |
+
This is `spaces.aoti_load_from_package_dir` with two changes, both forced by how this Space runs.
|
| 161 |
+
|
| 162 |
+
*Weights are read on the first forward, not at patch time.* `spaces.aoti_patch` snapshots `state_dict()` when it
|
| 163 |
+
patches, and `maybe_load` runs at startup, while the components are still on the host — `place()` only moves them
|
| 164 |
+
on the first request, because ZeroGPU cannot pack a startup-resident `Float8Tensor` (it packs CUDA tensors with
|
| 165 |
+
`aten.empty_like(..., pin_memory=True)`, which the subclass does not implement). `Module.to` rebinds `param.data`
|
| 166 |
+
to a fresh CUDA tensor, so a snapshot taken at startup keeps pointing at the host copies and the compiled block
|
| 167 |
+
would run against host memory. Reading the state dict on the first call instead picks it up wherever it now is.
|
| 168 |
+
|
| 169 |
+
*`temb` is padded on the way in*, to the fixed height the package was exported with — see `TEMB_ROWS`.
|
| 170 |
+
|
| 171 |
+
The clone-and-flatten here is `spaces.aoti_patch`'s own preparation, kept because a quantized width needs it: the
|
| 172 |
+
names it produces are the FQNs the package's constants were derived from. For an unquantized block it is a no-op
|
| 173 |
+
and the resulting names are exactly `blocks[0].state_dict()`, which is what `export_block` exported — the two
|
| 174 |
+
sides agree either way. What must *not* be mirrored is the clone on the export side under non-strict tracing; see
|
| 175 |
+
`export_block` for why that is the difference between a working package and a SIGSEGV.
|
| 176 |
+
"""
|
| 177 |
+
from spaces.zero.torch.aoti import LazyAOTIModel, _shallow_clone_module
|
| 178 |
+
from torch._functorch._aot_autograd.subclass_parametrization import unwrap_tensor_subclass_parameters
|
| 179 |
+
|
| 180 |
+
# `LazyAOTIModel` binds constants by intersecting `state_dict()` with `get_constant_fqns()` and
|
| 181 |
+
# silently keeps whatever it does not match, so a package whose constants were lifted anonymously
|
| 182 |
+
# binds nothing and the compiled block then dereferences constants nobody set — a SIGSEGV. This patch
|
| 183 |
+
# resolves those names through the `constant_aliases.json` the compile side writes, and **raises** a
|
| 184 |
+
# readable error if it still cannot. Purely protective: with a well-formed package it changes nothing,
|
| 185 |
+
# which is why a missing sidecar module is a warning rather than a failure.
|
| 186 |
+
try:
|
| 187 |
+
from spaces_constant_binding_patch import apply_spaces_constant_binding_patch
|
| 188 |
+
|
| 189 |
+
apply_spaces_constant_binding_patch()
|
| 190 |
+
except ImportError:
|
| 191 |
+
print("[h3-aoti] spaces_constant_binding_patch.py is missing; an unbindable constant would segfault", flush=True)
|
| 192 |
+
|
| 193 |
+
model = LazyAOTIModel(Path(package_dir) / "submodules" / BLOCK_CONTAINER / "package.pt2")
|
| 194 |
+
|
| 195 |
+
for block in getattr(transformer, BLOCK_CONTAINER):
|
| 196 |
+
bound: dict = {}
|
| 197 |
+
|
| 198 |
+
def forward(hidden_states, temb, *rest, _block=block, _bound=bound):
|
| 199 |
+
first = not _bound
|
| 200 |
+
if first:
|
| 201 |
+
clone = _shallow_clone_module(_block)
|
| 202 |
+
unwrap_tensor_subclass_parameters(clone)
|
| 203 |
+
_bound["weights"] = clone.state_dict()
|
| 204 |
+
return model(_bound["weights"], first, hidden_states, pad_temb(temb), *rest)
|
| 205 |
+
|
| 206 |
+
block.forward = forward
|
| 207 |
+
print(f"[h3-aoti] {len(getattr(transformer, BLOCK_CONTAINER))} blocks patched (temb padded to {TEMB_ROWS})", flush=True)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def maybe_load(transformer) -> None:
|
| 211 |
+
"""Patch the block stack with its compiled package. Once, and safe to call at **startup**.
|
| 212 |
+
|
| 213 |
+
Nothing here touches a GPU: the download is CPU work and the `.pt2` archive is not opened until the first forward,
|
| 214 |
+
which happens inside the `@spaces.GPU` call. Proven on the pool — `diffusers-internal-dev/minimax-h3-generator-aoti`
|
| 215 |
+
loads `bf16/torch2.11/sm120/dynamic` at startup, patches all 50 blocks, and generates.
|
| 216 |
+
"""
|
| 217 |
+
if not AOTI or id(transformer) in _LOADED:
|
| 218 |
+
return
|
| 219 |
+
|
| 220 |
+
import spaces
|
| 221 |
+
from huggingface_hub import snapshot_download
|
| 222 |
+
|
| 223 |
+
key = artifact_key()
|
| 224 |
+
print(f"[h3-aoti] loading {AOTI_REPO}:{key} ...", flush=True)
|
| 225 |
+
local = snapshot_download(
|
| 226 |
+
repo_id=AOTI_REPO,
|
| 227 |
+
repo_type=AOTI_REPO_TYPE,
|
| 228 |
+
allow_patterns=f"{key}/package/*",
|
| 229 |
+
token=os.environ.get("HF_TOKEN"),
|
| 230 |
+
)
|
| 231 |
+
package_dir = Path(local) / key / "package"
|
| 232 |
+
if not package_dir.is_dir():
|
| 233 |
+
raise RuntimeError(
|
| 234 |
+
f"No AoTI package at `{AOTI_REPO}:{key}/package`. Run the debug Space's Compile (AoTI) tab on this card "
|
| 235 |
+
f"with this `H3_QUANT`, or set `H3_AOTI=0`."
|
| 236 |
+
)
|
| 237 |
+
patch_blocks(transformer, package_dir)
|
| 238 |
+
_LOADED.add(id(transformer))
|
| 239 |
+
print(f"[h3-aoti] compiled blocks in place (temb padded to {TEMB_ROWS} rows)", flush=True)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def export_block(pipe, height: int, width: int, num_frames: int, prompt: str):
|
| 243 |
+
"""Capture one block call out of a real request and export it with a dynamic sequence dimension.
|
| 244 |
+
|
| 245 |
+
Runs on the GPU, after the transformer has been quantized and moved there: the export traces the quantized module,
|
| 246 |
+
and a package compiled for one quantization mode is meaningless for another.
|
| 247 |
+
"""
|
| 248 |
+
import torch
|
| 249 |
+
import spaces
|
| 250 |
+
|
| 251 |
+
import h3_core as h3
|
| 252 |
+
|
| 253 |
+
transformer = h3.transformer_of(pipe)
|
| 254 |
+
blocks = getattr(transformer, BLOCK_CONTAINER)
|
| 255 |
+
|
| 256 |
+
# Record every call the block receives over a short real run and keep the widest `temb`, rather than
|
| 257 |
+
# `spaces.aoti_capture`'s first-call-then-abort. The first call is the unrepresentative one: see `TEMB_ROWS`.
|
| 258 |
+
# Text encoding and packing run either way, which is the point — these are the real inputs.
|
| 259 |
+
original_forward = blocks[0].forward
|
| 260 |
+
widest = {"args": (), "kwargs": {}, "rows": -1}
|
| 261 |
+
seen = []
|
| 262 |
+
|
| 263 |
+
def recording(*args, **kwargs):
|
| 264 |
+
rows = int(args[1].shape[0]) if len(args) > 1 and hasattr(args[1], "shape") else -1
|
| 265 |
+
seen.append(rows)
|
| 266 |
+
if rows > widest["rows"]:
|
| 267 |
+
widest.update(args=args, kwargs=kwargs, rows=rows)
|
| 268 |
+
return original_forward(*args, **kwargs)
|
| 269 |
+
|
| 270 |
+
blocks[0].forward = recording
|
| 271 |
+
try:
|
| 272 |
+
pipe(
|
| 273 |
+
prompt=prompt,
|
| 274 |
+
height=height,
|
| 275 |
+
width=width,
|
| 276 |
+
num_frames=num_frames,
|
| 277 |
+
num_inference_steps=int(os.environ.get("H3_AOTI_CAPTURE_STEPS", "4")),
|
| 278 |
+
generator=torch.Generator("cpu").manual_seed(42),
|
| 279 |
+
)
|
| 280 |
+
finally:
|
| 281 |
+
blocks[0].forward = original_forward
|
| 282 |
+
call = type("Captured", (), widest)
|
| 283 |
+
if not call.args:
|
| 284 |
+
raise RuntimeError("Nothing was captured — the transformer block was never called.")
|
| 285 |
+
print(f"[h3-aoti] temb rows seen: {sorted(set(seen))}; exporting with {TEMB_ROWS} (padded)", flush=True)
|
| 286 |
+
|
| 287 |
+
# `block(hidden_states, temb, adaln_indices, rotary_emb, attention_mask)`:
|
| 288 |
+
# hidden_states (1, S, hidden)
|
| 289 |
+
# temb (num_distinct_timesteps, time_embed_dim)
|
| 290 |
+
# adaln_indices (S,)
|
| 291 |
+
# rotary_emb ((S, dim), (S, dim))
|
| 292 |
+
# attention_mask None for a padless sequence, which is what these pipelines build
|
| 293 |
+
#
|
| 294 |
+
# Only the sequence is asked for. `temb`'s row count is held constant by padding instead (see `TEMB_ROWS`), which
|
| 295 |
+
# is both cheaper to reason about and the only thing that works: `torch.export` specializes size-1 dimensions
|
| 296 |
+
# unconditionally, so a `Dim` on a dimension that is 1 at step 0 cannot be expressed at all.
|
| 297 |
+
if AOTI_SHAPE == "dynamic":
|
| 298 |
+
sequence = torch.export.Dim("sequence", min=2048, max=262144)
|
| 299 |
+
dynamic_shapes = ({1: sequence}, None, {0: sequence}, ({0: sequence}, {0: sequence}), None)
|
| 300 |
+
dynamic_shapes = dynamic_shapes[: len(call.args)]
|
| 301 |
+
else:
|
| 302 |
+
dynamic_shapes = None
|
| 303 |
+
|
| 304 |
+
# `temb` to its fixed height, so the AdaLN table the package bakes in is the one `maybe_load` will feed it.
|
| 305 |
+
args = (call.args[0], pad_temb(call.args[1]), *call.args[2:])
|
| 306 |
+
|
| 307 |
+
# WHICH MODULE, AND WHICH EXPORT MODE. This is the whole difference between a working package and a SIGSEGV.
|
| 308 |
+
#
|
| 309 |
+
# `spaces.aoti_patch` prepares the load side by shallow-cloning the module and flattening any tensor subclass, and
|
| 310 |
+
# the received wisdom is to do the identical thing before exporting so both sides derive the same constant FQNs.
|
| 311 |
+
# For a subclass that is genuinely necessary: inductor's constant handling wraps a constant back into
|
| 312 |
+
# `torch.nn.Parameter`, which rejects a non-floating dtype ("Only Tensors of floating point and complex dtype can
|
| 313 |
+
# require gradients"), so `Float8Tensor` / `NVFP4Tensor` parameters have to be flattened first.
|
| 314 |
+
#
|
| 315 |
+
# But a shallow clone exported in `torch.export`'s **default non-strict** mode duplicates every weight: the same
|
| 316 |
+
# tensor comes out once as a named `PARAMETER` and again as an anonymous `lifted_tensor_<N>` `CONSTANT_TENSOR`,
|
| 317 |
+
# 12 of each for this block, 1.2 GiB of them, `data_ptr()` proving the two sets alias. `LazyAOTIModel` binds by
|
| 318 |
+
# name, so the anonymous half binds to nothing and the compiled block dereferences constants nobody set. That is
|
| 319 |
+
# the crash that stalled this work, blamed first on accelerate's offload hooks and then on torchao's subclasses;
|
| 320 |
+
# it is neither. Measured on an rtx-pro-6000 Job at full size, torch 2.11, plain bfloat16, no subclass anywhere:
|
| 321 |
+
#
|
| 322 |
+
# live block, non-strict 12 PARAMETER, 0 CONSTANT_TENSOR <- what the shipped bf16 package used
|
| 323 |
+
# live block, strict 12 PARAMETER, 0 CONSTANT_TENSOR
|
| 324 |
+
# shallow clone, non-strict 12 PARAMETER, 12 CONSTANT_TENSOR <- the bug
|
| 325 |
+
# shallow clone, strict 12 PARAMETER, 0 CONSTANT_TENSOR
|
| 326 |
+
#
|
| 327 |
+
# So: export the **live block** whenever it has no subclass parameters to flatten, which is every unquantized
|
| 328 |
+
# width and, per the fp8 investigation, quantized ones whose weights are still registered parameters. Only fall
|
| 329 |
+
# back to the clone when flattening is actually needed, and then in `strict` mode, which is also clean. The clone
|
| 330 |
+
# is safe to skip for the live-block path precisely because there is nothing to unwrap: `state_dict()` names are
|
| 331 |
+
# then identical on both sides by construction.
|
| 332 |
+
from spaces.zero.torch.aoti import _shallow_clone_module
|
| 333 |
+
from torch._functorch._aot_autograd.subclass_parametrization import unwrap_tensor_subclass_parameters
|
| 334 |
+
|
| 335 |
+
subclassed = sorted({type(p).__name__ for p in blocks[0].parameters()} - {"Parameter"})
|
| 336 |
+
if subclassed:
|
| 337 |
+
block = _shallow_clone_module(blocks[0])
|
| 338 |
+
unwrap_tensor_subclass_parameters(block)
|
| 339 |
+
strict = True
|
| 340 |
+
print(f"[h3-aoti] tensor-subclass parameters {subclassed}: exporting a flattened clone, strict=True", flush=True)
|
| 341 |
+
else:
|
| 342 |
+
block = blocks[0]
|
| 343 |
+
strict = False
|
| 344 |
+
print("[h3-aoti] plain parameters: exporting the live block, non-strict", flush=True)
|
| 345 |
+
|
| 346 |
+
# `torch.export` only gives a lifted tensor a real FQN when it is a registered parameter or buffer; a tensor
|
| 347 |
+
# reached through a plain attribute becomes an anonymous constant the loader can never match against
|
| 348 |
+
# `state_dict()`. Re-registering such tensors as buffers is numerics-preserving — it changes how a tensor is
|
| 349 |
+
# registered, never the tensor and never the forward. A well-formed block has none, and this returns empty.
|
| 350 |
+
# Only ever on the clone: `register_loose_tensors` *re-registers* attributes, so running it on the live block would
|
| 351 |
+
# mutate the model the eager path uses. A `MiniMaxH3TransformerBlock` has no loose tensor attributes, so this is
|
| 352 |
+
# empty in practice and the live-block export needs nothing; if that ever changes, the warning below catches it.
|
| 353 |
+
if block is not blocks[0]:
|
| 354 |
+
try:
|
| 355 |
+
from spaces_constant_binding_patch import register_loose_tensors
|
| 356 |
+
|
| 357 |
+
if loose := register_loose_tensors(block):
|
| 358 |
+
print(f"[h3-aoti] re-registered {len(loose)} loose tensors as buffers: {loose[:6]}", flush=True)
|
| 359 |
+
except ImportError:
|
| 360 |
+
pass
|
| 361 |
+
|
| 362 |
+
print(f"[h3-aoti] exporting {type(blocks[0]).__name__}, shapes={AOTI_SHAPE}, strict={strict} ...", flush=True)
|
| 363 |
+
try:
|
| 364 |
+
exported = torch.export.export(block, args, call.kwargs or None, dynamic_shapes=dynamic_shapes, strict=strict)
|
| 365 |
+
except Exception as error:
|
| 366 |
+
# Dynamo refuses some modules it cannot trace. Non-strict is still worth attempting, with the duplication
|
| 367 |
+
# reported loudly below rather than left to segfault at load time.
|
| 368 |
+
if not strict:
|
| 369 |
+
raise
|
| 370 |
+
print(f"[h3-aoti] strict export failed ({type(error).__name__}: {error}); retrying non-strict", flush=True)
|
| 371 |
+
exported = torch.export.export(block, args, call.kwargs or None, dynamic_shapes=dynamic_shapes)
|
| 372 |
+
|
| 373 |
+
anonymous = [
|
| 374 |
+
spec.target for spec in exported.graph_signature.input_specs if spec.kind.name == "CONSTANT_TENSOR"
|
| 375 |
+
]
|
| 376 |
+
if anonymous:
|
| 377 |
+
print(
|
| 378 |
+
f"[h3-aoti] WARNING {len(anonymous)} constants lifted anonymously: {anonymous[:6]}. The loader binds by "
|
| 379 |
+
f"name, so this package will not bind them; `compile_and_save` writes the alias sidecar and "
|
| 380 |
+
f"`patch_blocks` raises rather than letting it segfault.",
|
| 381 |
+
flush=True,
|
| 382 |
+
)
|
| 383 |
+
return exported
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def compile_and_save(exported_program, destination: str | os.PathLike[str]) -> Path:
|
| 387 |
+
"""Inductor-compile the exported block into `<destination>/package/submodules/transformer_blocks/package.pt2`.
|
| 388 |
+
|
| 389 |
+
That layout is what `aoti_load_from_package_dir` walks: it resolves the submodule name to the transformer's
|
| 390 |
+
`transformer_blocks` `ModuleList` and, because a `ModuleList` is iterable, patches every one of the 50 blocks with
|
| 391 |
+
this single package.
|
| 392 |
+
"""
|
| 393 |
+
import spaces
|
| 394 |
+
|
| 395 |
+
package_dir = Path(destination) / "package"
|
| 396 |
+
print("[h3-aoti] inductor compile (minutes) ...", flush=True)
|
| 397 |
+
spaces.aoti_compile_and_save(package_dir, exported_program, submodule=BLOCK_CONTAINER)
|
| 398 |
+
|
| 399 |
+
# The compiled artifact keeps a constant's dtype, shape and slot index but drops its FQN when the
|
| 400 |
+
# export lifted it anonymously. The `ExportedProgram` still has the real names, so record the
|
| 401 |
+
# mapping next to the package while it is still available; the loader reads it back.
|
| 402 |
+
try:
|
| 403 |
+
from spaces_constant_binding_patch import write_constant_aliases
|
| 404 |
+
|
| 405 |
+
if sidecar := write_constant_aliases(package_dir, exported_program, submodule=BLOCK_CONTAINER):
|
| 406 |
+
print(f"[h3-aoti] constant alias sidecar written: {sidecar.name}", flush=True)
|
| 407 |
+
except ImportError:
|
| 408 |
+
pass
|
| 409 |
+
|
| 410 |
+
files = sorted(str(path.relative_to(package_dir)) for path in package_dir.rglob("*") if path.is_file())
|
| 411 |
+
print(f"[h3-aoti] package written: {files}", flush=True)
|
| 412 |
+
return package_dir
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def upload(package_dir: str | os.PathLike[str], key: str) -> str:
|
| 416 |
+
"""Push the package under its `<quant>/torch<X.Y>/sm<cc>/<shape>` key. CPU work — never inside GPU time."""
|
| 417 |
+
from huggingface_hub import HfApi
|
| 418 |
+
|
| 419 |
+
token = os.environ.get("HF_TOKEN")
|
| 420 |
+
if not token:
|
| 421 |
+
raise RuntimeError("`HF_TOKEN` is needed to push the AoTI package.")
|
| 422 |
+
api = HfApi(token=token)
|
| 423 |
+
api.create_repo(repo_id=AOTI_REPO, repo_type=AOTI_REPO_TYPE, private=True, exist_ok=True)
|
| 424 |
+
api.upload_folder(
|
| 425 |
+
folder_path=str(package_dir),
|
| 426 |
+
path_in_repo=f"{key}/package",
|
| 427 |
+
repo_id=AOTI_REPO,
|
| 428 |
+
repo_type=AOTI_REPO_TYPE,
|
| 429 |
+
commit_message=f"AoTI package for {key}",
|
| 430 |
+
)
|
| 431 |
+
return f"https://huggingface.co/{'datasets/' if AOTI_REPO_TYPE == 'dataset' else ''}{AOTI_REPO}/tree/main/{key}"
|