Spaces:
Running on Zero
Running on Zero
| """Packed on-disk storage for LTX-2.5 encoder precision plans. | |
| `ltx_fake_quantize` and `ltx_gptq` store quantized values expanded back to | |
| BF16, so a 4.5-bit plan still costs 26 GB on disk. This module is the missing | |
| second half: the same values in their native widths. | |
| The format is one safetensors file. A raw tensor keeps its checkpoint name. A | |
| quantized tensor is split into named parts: | |
| * nvfp4 - ``{name}::nvfp4_codes`` (uint8, two 4-bit codes per byte, even | |
| column in the low nibble; a code is ``sign << 3 | magnitude`` indexing | |
| ``E2M1_LEVELS``), ``{name}::nvfp4_group_scale`` (float8_e4m3fn, | |
| [out, in/16]), ``{name}::nvfp4_global_scale`` (float32 scalar), and - when | |
| the GPTQ path smoothed the tensor - ``{name}::pre_quant_scale`` (float32, | |
| [in], the AWQ scale the stored weight was *divided* by). | |
| * int8 - ``{name}::int8`` and ``{name}::int8_scale`` (float32, [out]). | |
| Exactness is the design constraint, not an aspiration: unpacking replays the | |
| producers' arithmetic - ``(sign * level) * effective`` then the pre-scale | |
| division, in float32, cast to the stored dtype last - so a packed tensor | |
| dequantizes to the same values the BF16 fake-quant file would have carried. | |
| The one tolerated difference is that int8 cannot store a negative zero, so a | |
| ``-0.0`` produced by ``round()`` collapses to ``+0.0``; ``torch.equal`` treats | |
| the two as equal and the packer counts them rather than hiding them. | |
| Group scales are recorded as the exact float8 bytes the quantizer used, not | |
| recomputed from the output. Recomputing them is impossible in general: the | |
| GPTQ column loop derives each group's scale from weights that were already | |
| compensated, and the AWQ division afterwards destroys the grid alignment that | |
| recovery would need. That is why packing happens inside the build | |
| (`ltx_gptq --packed-output`) instead of as a post-pass over the artifact. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import torch | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| from ltx_fake_quantize import ( | |
| E2M1_LEVELS, | |
| E2M1_MIDPOINTS, | |
| E4M3_MAX, | |
| GPU_TENSOR_LIMIT, | |
| NVFP4_GROUP, | |
| ) | |
| FORMAT_VERSION = "haverbex-packed-v1" | |
| #: Part suffixes. `::` cannot appear in a checkpoint tensor name, so packed | |
| #: parts can never collide with a raw tensor. | |
| CODES = "::nvfp4_codes" | |
| GROUP_SCALE = "::nvfp4_group_scale" | |
| GLOBAL_SCALE = "::nvfp4_global_scale" | |
| PRE_SCALE = "::pre_quant_scale" | |
| INT8 = "::int8" | |
| INT8_SCALE = "::int8_scale" | |
| _SUFFIXES = (CODES, GROUP_SCALE, GLOBAL_SCALE, PRE_SCALE, INT8, INT8_SCALE) | |
| #: Parts indexed by output row, so a chunked read can slice them. The other two | |
| #: are not: `GLOBAL_SCALE` is a scalar and `PRE_SCALE` is per *input* channel. | |
| _ROW_SLICEABLE = (CODES, GROUP_SCALE, INT8, INT8_SCALE) | |
| #: Part suffix -> buffer name on a resident module. `::` is illegal in a | |
| #: `register_buffer` name, so the mapping cannot be derived. | |
| _BUFFER_NAMES = {CODES: "codes", GROUP_SCALE: "group_scale", | |
| GLOBAL_SCALE: "global_scale", PRE_SCALE: "pre_scale", | |
| INT8: "ints", INT8_SCALE: "int8_scale"} | |
| def nibble_pack(codes: torch.Tensor) -> torch.Tensor: | |
| """[out, in] uint8 (values 0..15) -> [out, in/2], even column low nibble.""" | |
| if codes.shape[-1] % 2: | |
| raise ValueError(f"odd inner width {codes.shape[-1]} cannot nibble-pack") | |
| return codes[..., 0::2] | (codes[..., 1::2] << 4) | |
| def nibble_unpack(packed: torch.Tensor) -> torch.Tensor: | |
| out = torch.empty(*packed.shape[:-1], packed.shape[-1] * 2, dtype=torch.uint8, | |
| device=packed.device) | |
| out[..., 0::2] = packed & 0x0F | |
| out[..., 1::2] = packed >> 4 | |
| return out | |
| def encode_nvfp4_column(normalized: torch.Tensor, codes: torch.Tensor) -> torch.Tensor: | |
| """4-bit code for one already-bucketized column: sign bit over magnitude. | |
| `torch.sign` maps an exact zero to 0, which multiplies out to ``+0.0``; the | |
| encoding gives it sign bit 0 so decode lands on the same ``+0.0``. | |
| """ | |
| return codes.to(torch.uint8) | ((normalized < 0).to(torch.uint8) << 3) | |
| def decode_nvfp4(codes: torch.Tensor, group_scale: torch.Tensor, | |
| global_scale: torch.Tensor, | |
| pre_scale: torch.Tensor | None) -> torch.Tensor: | |
| """Replay of `NVFP4Column.quantize` / `quantize_nvfp4`, in float32. | |
| `levels` is built on the codes' device: at load time everything is on the | |
| CPU, but a `PackedLinear` decodes wherever its weight lives. | |
| """ | |
| levels = torch.tensor(E2M1_LEVELS, dtype=torch.float32, device=codes.device) | |
| sign = torch.where((codes & 0x8) != 0, -1.0, 1.0) | |
| magnitude = levels[(codes & 0x7).long()] | |
| effective = group_scale.float() * global_scale.float() | |
| effective = torch.where(effective > 0, effective, torch.ones_like(effective)) | |
| out_features, in_features = codes.shape | |
| grid = (sign * magnitude).reshape(out_features, in_features // NVFP4_GROUP, | |
| NVFP4_GROUP) | |
| grid = (grid * effective.unsqueeze(-1)).reshape(out_features, in_features) | |
| if pre_scale is not None: | |
| grid = grid / pre_scale.float() | |
| return grid | |
| def decode_int8(ints: torch.Tensor, scale: torch.Tensor, | |
| pre_scale: torch.Tensor | None) -> torch.Tensor: | |
| """Replay of `quantize_int8` / `Int8Column.quantize`, in float32.""" | |
| grid = ints.float() * scale.float().unsqueeze(-1) | |
| if pre_scale is not None: | |
| grid = grid / pre_scale.float() | |
| return grid | |
| def pack_rtn_nvfp4(w: torch.Tensor) -> tuple[torch.Tensor, dict]: | |
| """`ltx_fake_quantize.quantize_nvfp4` with the codes and scales kept. | |
| Returns the dequantized float32 tensor (identical to what the fake | |
| quantizer produces) and the packed parts. | |
| """ | |
| out_features, in_features = w.shape | |
| if in_features % NVFP4_GROUP: | |
| raise ValueError(f"nvfp4 needs a multiple of {NVFP4_GROUP}, got {in_features}") | |
| amax = w.abs().amax() | |
| if amax == 0: | |
| raise ValueError("all-zero tensor should be stored raw, not packed") | |
| global_scale = amax / (E2M1_LEVELS[-1] * E4M3_MAX) | |
| groups = w.reshape(out_features, in_features // NVFP4_GROUP, NVFP4_GROUP) | |
| group_amax = groups.abs().amax(dim=-1, keepdim=True) | |
| scale = (group_amax / E2M1_LEVELS[-1] / global_scale).to(torch.float8_e4m3fn) | |
| effective = scale.float() * global_scale | |
| effective = torch.where(effective > 0, effective, torch.ones_like(effective)) | |
| levels = torch.tensor(E2M1_LEVELS, device=w.device, dtype=w.dtype) | |
| midpoints = torch.tensor(E2M1_MIDPOINTS, device=w.device, dtype=w.dtype) | |
| normalized = groups / effective | |
| codes = torch.bucketize(normalized.abs(), midpoints, out_int32=True) | |
| dequant = (torch.sign(normalized) * levels[codes] * effective).reshape( | |
| out_features, in_features) | |
| packed_codes = (codes.to(torch.uint8) | |
| | ((normalized < 0).to(torch.uint8) << 3)).reshape( | |
| out_features, in_features) | |
| parts = { | |
| CODES: nibble_pack(packed_codes).cpu(), | |
| GROUP_SCALE: scale.squeeze(-1).cpu(), | |
| GLOBAL_SCALE: global_scale.detach().float().reshape(1).cpu(), | |
| } | |
| return dequant, parts | |
| def pack_rtn(writer: PackWriter, name: str, tensor: torch.Tensor, | |
| width: float, device: str) -> torch.Tensor: | |
| """`ltx_fake_quantize.quantize_tensor`, but keeping the codes. | |
| Returns the fake-quantized tensor in the original dtype so the caller's | |
| BF16 write path is unchanged. Same device policy as the original: a tensor | |
| whose float32 form exceeds `GPU_TENSOR_LIMIT` is quantized on the CPU. | |
| """ | |
| original = tensor.dtype | |
| where = "cpu" if tensor.numel() * 4 > GPU_TENSOR_LIMIT else device | |
| w = tensor.to(where, torch.float32) | |
| if width == 4.5: | |
| if w.abs().amax() == 0: | |
| # quantize_nvfp4 returns an all-zero tensor unchanged; store it raw | |
| writer.store_raw(name, tensor) | |
| return tensor | |
| dequant, parts = pack_rtn_nvfp4(w) | |
| result = dequant.to("cpu", original) | |
| writer.store_nvfp4(name, parts, result) | |
| else: | |
| dequant, parts = pack_rtn_int8(w) | |
| result = dequant.to("cpu", original) | |
| writer.store_int8(name, parts, result) | |
| return result | |
| def pack_rtn_int8(w: torch.Tensor) -> tuple[torch.Tensor, dict]: | |
| """`ltx_fake_quantize.quantize_int8` with the integers kept.""" | |
| scale = w.abs().amax(dim=-1, keepdim=True) / 127.0 | |
| scale = torch.where(scale > 0, scale, torch.ones_like(scale)) | |
| ints = (w / scale).round().clamp_(-127, 127) | |
| dequant = ints * scale | |
| parts = { | |
| INT8: ints.to(torch.int8).cpu(), | |
| INT8_SCALE: scale.squeeze(-1).float().cpu(), | |
| } | |
| return dequant, parts | |
| class PackWriter: | |
| """Accumulates packed parts and raw tensors, then writes one file. | |
| Held in RAM rather than streamed: the whole point of the format is that | |
| the payload is ~8.5 GB, which fits beside the build. Every quantized store | |
| verifies round-trip equality against the reference tensor before | |
| accepting it - a packed file that does not reproduce its builder's values | |
| must not be creatable through this class. | |
| """ | |
| def __init__(self) -> None: | |
| self.tensors: dict[str, torch.Tensor] = {} | |
| self.kinds: dict[str, str] = {} | |
| self.negative_zero_collapses = 0 | |
| def store_raw(self, name: str, tensor: torch.Tensor) -> None: | |
| if any(s in name for s in _SUFFIXES): | |
| raise ValueError(f"raw name collides with a part suffix: {name}") | |
| self.tensors[name] = tensor.detach().contiguous().cpu() | |
| self.kinds[name] = "raw" | |
| def _verify(self, name: str, reference: torch.Tensor) -> None: | |
| got = unpack_tensor(self.tensors, name, self.kinds[name], | |
| reference.dtype) | |
| if not torch.equal(got, reference.cpu()): | |
| diff = (got.float() - reference.float().cpu()).abs() | |
| raise SystemExit( | |
| f"packed round-trip mismatch on {name}: max {diff.max():.3e} at " | |
| f"{int(diff.argmax())} - refusing to write a lossy pack") | |
| signs = got.signbit() != reference.cpu().signbit() | |
| self.negative_zero_collapses += int(signs.sum()) | |
| def store_nvfp4(self, name: str, parts: dict, reference: torch.Tensor, | |
| pre_scale: torch.Tensor | None = None) -> None: | |
| for suffix, tensor in parts.items(): | |
| self.tensors[name + suffix] = tensor.contiguous() | |
| if pre_scale is not None: | |
| self.tensors[name + PRE_SCALE] = pre_scale.detach().float().cpu() | |
| self.kinds[name] = "nvfp4" | |
| self._verify(name, reference) | |
| def store_int8(self, name: str, parts: dict, reference: torch.Tensor, | |
| pre_scale: torch.Tensor | None = None) -> None: | |
| for suffix, tensor in parts.items(): | |
| self.tensors[name + suffix] = tensor.contiguous() | |
| if pre_scale is not None: | |
| self.tensors[name + PRE_SCALE] = pre_scale.detach().float().cpu() | |
| self.kinds[name] = "int8" | |
| self._verify(name, reference) | |
| def save(self, path: Path, metadata: dict[str, str]) -> None: | |
| from safetensors.torch import save_file | |
| payload = { | |
| "format": FORMAT_VERSION, | |
| "kinds": json.dumps(self.kinds, separators=(",", ":")), | |
| "negative_zero_collapses": str(self.negative_zero_collapses), | |
| **metadata, | |
| } | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| save_file(self.tensors, str(path), metadata=payload) | |
| def unpack_tensor(tensors: dict, name: str, kind: str, | |
| dtype: torch.dtype) -> torch.Tensor: | |
| if kind == "raw": | |
| return tensors[name] | |
| pre = tensors.get(name + PRE_SCALE) | |
| if kind == "nvfp4": | |
| grid = decode_nvfp4(nibble_unpack(tensors[name + CODES]), | |
| tensors[name + GROUP_SCALE], | |
| tensors[name + GLOBAL_SCALE], | |
| pre) | |
| elif kind == "int8": | |
| grid = decode_int8(tensors[name + INT8], tensors[name + INT8_SCALE], pre) | |
| else: | |
| raise ValueError(f"unknown kind {kind!r} for {name}") | |
| return grid.to(dtype) | |
| class PackedCheckpoint: | |
| """Read-side API mirroring `safetensors.safe_open` for packed files.""" | |
| def __init__(self, path: Path | str) -> None: | |
| from safetensors import safe_open | |
| self.path = Path(path) | |
| self._file = safe_open(str(self.path), framework="pt") | |
| self.metadata = self._file.metadata() or {} | |
| if self.metadata.get("format") != FORMAT_VERSION: | |
| raise SystemExit( | |
| f"{path}: format {self.metadata.get('format')!r}, " | |
| f"this reader speaks {FORMAT_VERSION}") | |
| self.kinds: dict[str, str] = json.loads(self.metadata["kinds"]) | |
| self._present = set(self._file.keys()) | |
| def __enter__(self) -> PackedCheckpoint: | |
| return self | |
| def __exit__(self, *exc) -> None: | |
| return None | |
| def keys(self) -> list[str]: | |
| return list(self.kinds) | |
| def get_tensor(self, name: str, dtype: torch.dtype = torch.bfloat16): | |
| kind = self.kinds[name] | |
| if kind == "raw": | |
| return self._file.get_tensor(name) | |
| parts = {name + s: self._file.get_tensor(name + s) | |
| for s in _SUFFIXES if name + s in self._present} | |
| return unpack_tensor(parts, name, kind, dtype) | |
| def get_parts(self, name: str) -> dict: | |
| """The stored parts of a packed tensor, keyed by bare suffix, undecoded. | |
| What a resident module needs: the bytes as written, with no decode and | |
| no reassembly. | |
| """ | |
| return {s: self._file.get_tensor(name + s) | |
| for s in _SUFFIXES if name + s in self._present} | |
| def row_count(self, name: str) -> int: | |
| """Output rows of a packed tensor, without decoding it.""" | |
| kind = self.kinds[name] | |
| key = name if kind == "raw" else name + (CODES if kind == "nvfp4" else INT8) | |
| return self._file.get_slice(key).get_shape()[0] | |
| def get_tensor_rows(self, name: str, start: int, stop: int, | |
| dtype: torch.dtype = torch.bfloat16): | |
| """Output rows `[start, stop)` of a tensor, decoding only those rows. | |
| Lets a caller work through a table that does not fit beside the model - | |
| the LTX aggregates are 3.08 GB each in float32. Row-sliced parts are | |
| the ones indexed by output channel; the global scale is a scalar and | |
| the AWQ pre-scale is per *input* channel, so both are read whole. | |
| """ | |
| kind = self.kinds[name] | |
| if kind == "raw": | |
| return self._file.get_slice(name)[start:stop].to(dtype) | |
| parts = {} | |
| for suffix in _SUFFIXES: | |
| key = name + suffix | |
| if key not in self._present: | |
| continue | |
| parts[key] = (self._file.get_slice(key)[start:stop] | |
| if suffix in _ROW_SLICEABLE | |
| else self._file.get_tensor(key)) | |
| return unpack_tensor(parts, name, kind, dtype) | |
| def open_maybe_packed(path: Path | str): | |
| """`safe_open` for BF16 checkpoints, `PackedCheckpoint` for packed ones. | |
| Both expose `keys()` and `get_tensor(name)`, which is all the aggregate | |
| and asset readers use. Detection is by content, not extension: a packed | |
| file carries `format` metadata that a plain checkpoint does not. | |
| """ | |
| from safetensors import safe_open | |
| handle = safe_open(str(path), framework="pt") | |
| if (handle.metadata() or {}).get("format") == FORMAT_VERSION: | |
| return PackedCheckpoint(path) | |
| return handle | |
| def install_resident(model, reader: PackedCheckpoint, wanted: dict, rename, | |
| device: str | None = None, fold_pre_scale: bool = False): | |
| """Replace the quantized modules of a meta-device model with packed ones. | |
| Two passes, because a module has to exist before its bias can be fed into | |
| it: first swap every module whose weight is packed, then feed the raw | |
| tensors - norms, layer scalars, and any bias - into whatever module now | |
| sits at that path. | |
| The coverage guard is the same one the dequantized path uses, and it is | |
| what makes a silent rename failure impossible: every parameter the model | |
| declared must be accounted for, either by a swap or by a raw feed. | |
| """ | |
| from accelerate.utils import set_module_tensor_to_device | |
| if device is None: | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| swapped, raw_names = set(), [] | |
| for source_name in reader.keys(): # noqa: SIM118 - PackedCheckpoint, not a dict | |
| param_name = rename(source_name) | |
| if param_name not in wanted: | |
| continue # aggregates and asset blobs live outside the module tree | |
| if reader.kinds[source_name] == "raw": | |
| raw_names.append((source_name, param_name)) | |
| continue | |
| if not param_name.endswith(".weight"): | |
| raise SystemExit( | |
| f"{source_name} is packed but renames to {param_name}, which is " | |
| "not a module weight; a resident build has nowhere to put it") | |
| path = param_name[: -len(".weight")] | |
| module = model.get_submodule(path) | |
| parts = {k: v.to(device) for k, v in reader.get_parts(source_name).items()} | |
| if isinstance(module, torch.nn.Embedding): | |
| # Subclasses carry forward behaviour a bare gather would lose - | |
| # Gemma's scales by ~sqrt(hidden). Anything else must stop the build. | |
| extra = set(dict(module.named_buffers()))- {"embed_scale"} | |
| if type(module) is not torch.nn.Embedding and extra: | |
| raise SystemExit( | |
| f"{path} is a {type(module).__name__} carrying {sorted(extra)}; " | |
| "PackedEmbedding only reproduces embed_scale") | |
| packed = PackedEmbedding(parts[INT8], parts[INT8_SCALE], | |
| embed_scale=getattr(module, "embed_scale", None), | |
| padding_idx=module.padding_idx) | |
| elif isinstance(module, torch.nn.Linear): | |
| packed = PackedLinear(reader.kinds[source_name], parts, | |
| module.out_features, module.in_features, | |
| fold_pre_scale=fold_pre_scale) | |
| else: | |
| raise SystemExit(f"{path} is a {type(module).__name__}, and this " | |
| "build only knows how to pack Linear and Embedding") | |
| parent_path, _, attribute = path.rpartition(".") | |
| setattr(model.get_submodule(parent_path) if parent_path else model, | |
| attribute, packed) | |
| swapped.add(param_name) | |
| # Second pass, after every swap: a bias whose module was replaced lands on | |
| # the new module's buffer, which only exists once the swap has happened. | |
| for source_name, param_name in raw_names: | |
| value = reader.get_tensor(source_name, wanted[param_name].dtype).to(device) | |
| path, _, attribute = param_name.rpartition(".") | |
| owner = model.get_submodule(path) if path else model | |
| if isinstance(owner, (PackedLinear, PackedEmbedding)): | |
| # `set_module_tensor_to_device` reads the current value to find its | |
| # device, and a packed module declares `bias` as a None buffer. | |
| owner.register_buffer(attribute, value, persistent=False) | |
| else: | |
| set_module_tensor_to_device(model, param_name, device, value=value) | |
| assigned = swapped | {p for _, p in raw_names} | |
| missing = [n for n in wanted if n not in assigned and "rotary" not in n] | |
| if missing: | |
| raise SystemExit( | |
| f"{len(missing)} parameters were not fed from the packed file, " | |
| f"e.g. {missing[:5]}. The packed names do not match the model; " | |
| "extend LTX_RENAMES.") | |
| model.eval() | |
| print(f"resident packed model on {device}: " | |
| f"{resident_bytes(model) / 2**30:.3f} GiB", flush=True) | |
| return model | |
| def resident_bytes(model) -> int: | |
| """Everything the model holds: packed buffers plus whatever stayed dense. | |
| Counted once. A packed module's parts are registered buffers, so walking | |
| `model.buffers()` after adding `packed_bytes()` counts them twice - which | |
| is what the first A7 run reported (13.4 GiB against an allocator peak of | |
| 7.75 GiB). Packed buffers are collected by identity first and skipped in | |
| the dense pass. | |
| """ | |
| seen, total = set(), 0 | |
| for module in model.modules(): | |
| if isinstance(module, (PackedLinear, PackedEmbedding)): | |
| total += module.packed_bytes() | |
| for name in (*_BUFFER_NAMES.values(), "bias"): | |
| tensor = getattr(module, name, None) | |
| if tensor is not None: | |
| seen.add(id(tensor)) | |
| for tensor in list(model.parameters()) + list(model.buffers()): | |
| if id(tensor) in seen or tensor.device.type == "meta": | |
| continue | |
| seen.add(id(tensor)) | |
| total += tensor.numel() * tensor.element_size() | |
| return total | |
| def check_gpu_kernels(device: str | None = None) -> None: | |
| """Refuse a wheel that has no kernels for this card, and say what to install. | |
| Nothing in this format needs anything unusual from a GPU - no fp8 units, no | |
| minimum compute capability, no bf16 tensor cores. What can be missing is | |
| PyTorch's own kernels: the current default wheel on PyPI is a cu130 build, | |
| and cu130 dropped Volta. | |
| Left alone, that surfaces as | |
| CUDA error: no kernel image is available for execution on the device | |
| raised from the first kernel launch - which is after an 8.46 GB load, inside | |
| somebody else's library, and long after `torch.cuda.is_available()` returned | |
| True. Checking `get_arch_list` costs nothing and moves the failure to the | |
| place where the fix makes sense. | |
| """ | |
| if device is not None and not str(device).startswith("cuda"): | |
| return | |
| if not torch.cuda.is_available(): | |
| return | |
| major, minor = torch.cuda.get_device_capability(0) | |
| arch = f"sm_{major}{minor}" | |
| compiled = torch.cuda.get_arch_list() | |
| # An empty list means a build that does not report them; do not guess. | |
| if not compiled or arch in compiled: | |
| return | |
| name = torch.cuda.get_device_name(0) | |
| raise SystemExit( | |
| f"this torch ({torch.__version__}) has no kernels for {name} ({arch}).\n" | |
| f"It was built for {', '.join(compiled)}, and the first CUDA op would " | |
| f"fail with 'no kernel image is available for execution on the device'.\n" | |
| f"The model is fine - it needs no custom kernels. Install a torch built " | |
| f"for your card, e.g. for {arch}:\n" | |
| f" pip install torch --index-url https://download.pytorch.org/whl/cu128\n" | |
| f"or pass device='cpu' to load without touching the GPU.") | |
| def load_packed_model(model_dir: str, packed_path: Path | str, | |
| gpu_budget: str | None = None, resident: bool = False, | |
| fold_pre_scale: bool = False, device: str | None = None): | |
| """Build the encoder from a packed file, split across GPU and CPU. | |
| Mirrors what `AutoModel.from_pretrained(..., device_map="auto")` does for | |
| the BF16 checkpoint: same conversion-mapping renames, same guard that | |
| every parameter was actually fed from the file. Dequantization happens | |
| tensor-by-tensor, so peak memory is one dequantized tensor above the | |
| final footprint - the 26 GB BF16 file never exists. | |
| Note what the default does and does not save. The weights land as BF16, so | |
| the *resident* footprint is the same 26 GB the BF16 checkpoint would take; | |
| what packing buys there is disk and the load-time peak. `gpu_budget` | |
| defaults to 13 GiB - what is left of a 16 GB card - and `LTX_PACKED_GPU_BUDGET` | |
| raises it on a larger card, where the whole encoder fits and the CPU | |
| offload that budget forces is pure slowdown. | |
| With `resident`, the quantized tensors stay in their native widths behind | |
| `PackedLinear` / `PackedEmbedding` and no device map is needed: the model | |
| costs what the file costs. `fold_pre_scale` is passed through to the | |
| linears and is not bit-identical - see `PackedLinear`. | |
| """ | |
| import os | |
| import re | |
| check_gpu_kernels(device) | |
| if gpu_budget is None: | |
| gpu_budget = os.environ.get("LTX_PACKED_GPU_BUDGET", "13GiB") | |
| from accelerate import infer_auto_device_map, init_empty_weights | |
| from accelerate.utils import set_module_tensor_to_device | |
| from ltx_prompt_embedding_gate import register_ltx_renames | |
| from transformers import AutoConfig, AutoModel | |
| from transformers.conversion_mapping import get_checkpoint_conversion_mapping | |
| register_ltx_renames() | |
| rules = get_checkpoint_conversion_mapping("gemma4_unified") or [] | |
| def rename(name: str) -> str: | |
| for rule in rules: | |
| sources = rule.source_patterns | |
| targets = rule.target_patterns | |
| if isinstance(sources, str): | |
| sources, targets = [sources], [targets] | |
| for source, target in zip(sources, targets, strict=True): | |
| new = re.sub(source, target, name) | |
| if new != name: | |
| return new | |
| return name | |
| config = AutoConfig.from_pretrained(model_dir) | |
| # `from_config` does not resolve the attention implementation the way | |
| # `from_pretrained` does; left unset, a standalone build falls back to | |
| # eager and mis-applies the boolean SDPA mask (see ltx_gptq). | |
| config._attn_implementation = "sdpa" | |
| if hasattr(config, "text_config"): | |
| config.text_config._attn_implementation = "sdpa" | |
| with init_empty_weights(): | |
| model = AutoModel.from_config(config) | |
| model = model.to(torch.bfloat16) | |
| reader = PackedCheckpoint(packed_path) | |
| wanted = dict(model.state_dict()) | |
| if resident: | |
| return install_resident(model, reader, wanted, rename, | |
| device=device, fold_pre_scale=fold_pre_scale) | |
| device_map = infer_auto_device_map( | |
| model, max_memory={0: gpu_budget, "cpu": "40GiB"}, | |
| dtype=torch.bfloat16, no_split_module_classes=["Gemma4UnifiedTextDecoderLayer"]) | |
| # `set_module_tensor_to_device` places tensors but installs no hooks, so a | |
| # split map produces a model that only fails once a forward crosses the | |
| # boundary - as a device mismatch deep inside a layernorm. Refuse it here | |
| # instead, and say what to do about it. | |
| placements = set(device_map.values()) | |
| if len(placements) > 1: | |
| raise SystemExit( | |
| f"the packed model does not fit in {gpu_budget} and would be split " | |
| # str(): a device map mixes GPU ordinals with "cpu", and sorting | |
| # those against each other is a TypeError - which is how this | |
| # message first announced itself. | |
| f"across {sorted(map(str, placements))}. This loader dispatches no " | |
| "hooks, so a " | |
| "split model raises mid-forward. Raise LTX_PACKED_GPU_BUDGET, or use " | |
| "resident=True, which needs only what the file costs.") | |
| def target_device(param_name: str): | |
| candidate = param_name | |
| while candidate: | |
| if candidate in device_map: | |
| return device_map[candidate] | |
| candidate = candidate.rsplit(".", 1)[0] if "." in candidate else "" | |
| return device_map.get("", "cpu") | |
| assigned = set() | |
| for source_name in reader.keys(): # noqa: SIM118 - PackedCheckpoint, not a dict | |
| param_name = rename(source_name) | |
| if param_name not in wanted: | |
| continue # aggregates and asset blobs live outside the module tree | |
| value = reader.get_tensor(source_name, wanted[param_name].dtype) | |
| set_module_tensor_to_device(model, param_name, target_device(param_name), | |
| value=value) | |
| assigned.add(param_name) | |
| missing = [name for name in wanted | |
| if name not in assigned and "rotary" not in name] | |
| if missing: | |
| raise SystemExit( | |
| f"{len(missing)} parameters were not fed from {packed_path}, " | |
| f"e.g. {missing[:5]}. The packed names do not match the model; " | |
| "extend LTX_RENAMES.") | |
| model.eval() | |
| return model | |
| # --------------------------------------------------------------------------- | |
| # Resident modules | |
| # | |
| # `load_packed_model` dequantizes to BF16, which saves the load-time peak and | |
| # nothing else: the model that comes out is the same 26.264 GB it always was. | |
| # These modules keep the packed widths in memory and dequantize inside | |
| # `forward`, which is what turns 8.463 GB on disk into 8.463 GB resident. | |
| # | |
| # Affordable here because the encoder runs once per prompt - no KV cache, no | |
| # autoregressive loop. Measured on this checkpoint's real shapes at 1024 | |
| # tokens, a full dequantize costs 9-12% of the matmul it feeds. | |
| # --------------------------------------------------------------------------- | |
| class PackedLinear(torch.nn.Module): | |
| """`nn.Linear` whose weight is stored in its native widths. | |
| `forward` rebuilds the weight with the same arithmetic `unpack_tensor` uses | |
| at load time, so its output is bit-identical to the dequantized model's. | |
| That equality is the point: without it a resident run cannot be compared | |
| against any figure recorded from the dequantized path. | |
| `fold_pre_scale` trades the equality for speed. AWQ smoothing is | |
| ``W' = W * s`` and the stored weight carries the closing ``/ s``, so | |
| ``x @ (Q/s).T == (x/s) @ Q.T`` - the division can move onto the activation | |
| and shrink an ``[out, in]`` elementwise pass to a ``[tokens, in]`` one. It | |
| changes float rounding order, so it is off by default and its difference is | |
| measured rather than assumed. | |
| """ | |
| def __init__(self, kind: str, parts: dict, out_features: int, | |
| in_features: int, bias: torch.Tensor | None = None, | |
| dtype: torch.dtype = torch.bfloat16, | |
| fold_pre_scale: bool = False) -> None: | |
| super().__init__() | |
| if kind not in ("nvfp4", "int8"): | |
| raise ValueError(f"{kind!r} is not a packed weight kind") | |
| self.kind = kind | |
| self.out_features = out_features | |
| self.in_features = in_features | |
| self.compute_dtype = dtype | |
| for suffix, buffer in _BUFFER_NAMES.items(): | |
| tensor = parts.get(name_part(suffix, parts)) | |
| self.register_buffer(buffer, tensor, persistent=False) | |
| self.register_buffer("bias", None if bias is None else bias.detach(), | |
| persistent=False) | |
| self.fold_pre_scale = bool(fold_pre_scale) and self.pre_scale is not None | |
| def packed_bytes(self) -> int: | |
| """What this module actually costs, for a device map that cannot infer it.""" | |
| total = 0 | |
| for buffer in (*_BUFFER_NAMES.values(), "bias"): | |
| tensor = getattr(self, buffer, None) | |
| if tensor is not None: | |
| total += tensor.numel() * tensor.element_size() | |
| return total | |
| def weight(self) -> torch.Tensor: | |
| """The dequantized weight. | |
| A property rather than a buffer so that code reaching for | |
| `module.weight` - `transformers` does, in places - still works, at the | |
| cost of materializing it for that one call. | |
| """ | |
| return self.dequantize() | |
| def dequantize(self, pre_scale: bool = True) -> torch.Tensor: | |
| pre = self.pre_scale if (pre_scale and not self.fold_pre_scale) else None | |
| if self.kind == "nvfp4": | |
| grid = decode_nvfp4(nibble_unpack(self.codes), self.group_scale, | |
| self.global_scale, pre) | |
| else: | |
| grid = decode_int8(self.ints, self.int8_scale, pre) | |
| return grid.to(self.compute_dtype) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| if self.fold_pre_scale: | |
| x = x / self.pre_scale.to(x.dtype) | |
| return torch.nn.functional.linear(x, self.dequantize(), self.bias) | |
| def extra_repr(self) -> str: | |
| return (f"in_features={self.in_features}, out_features={self.out_features}, " | |
| f"kind={self.kind}, packed={self.packed_bytes() / 2**20:.1f} MiB" | |
| + (", folded" if self.fold_pre_scale else "")) | |
| class PackedEmbedding(torch.nn.Module): | |
| """`nn.Embedding` over int8 rows with a per-row scale. | |
| `model.embed_tokens` is [262144, 3840] - 1.008 GB packed against 2.016 GB | |
| BF16, the largest single tensor in the file, and the one DmitryDB's recipe | |
| keeps in BF16 as a precision island. Quantizing it is most of our disk | |
| advantage, and half of that is given back if it dequantizes at load. | |
| Gathering before scaling means only the rows a prompt touches are ever | |
| dequantized, which is a few thousand of 262144. | |
| `embed_scale` is not optional in practice. Gemma's embedding is a | |
| `Gemma4UnifiedTextScaledWordEmbedding`, whose forward is | |
| ``super().forward(ids) * embed_scale`` with `embed_scale` around sqrt(3840). | |
| Replacing the module without carrying that factor would drop every | |
| embedding by ~62x and still produce plausible-looking tensors, so the caller | |
| must read it off the module it is replacing. | |
| """ | |
| def __init__(self, ints: torch.Tensor, scale: torch.Tensor, | |
| dtype: torch.dtype = torch.bfloat16, | |
| embed_scale: torch.Tensor | None = None, | |
| padding_idx: int | None = None) -> None: | |
| super().__init__() | |
| self.num_embeddings, self.embedding_dim = ints.shape | |
| self.compute_dtype = dtype | |
| self.padding_idx = padding_idx | |
| self.register_buffer("ints", ints, persistent=False) | |
| self.register_buffer("int8_scale", scale, persistent=False) | |
| self.register_buffer("embed_scale", embed_scale, persistent=False) | |
| def packed_bytes(self) -> int: | |
| return (self.ints.numel() * self.ints.element_size() | |
| + self.int8_scale.numel() * self.int8_scale.element_size()) | |
| def weight(self) -> torch.Tensor: | |
| return decode_int8(self.ints, self.int8_scale, None).to(self.compute_dtype) | |
| def forward(self, index: torch.Tensor) -> torch.Tensor: | |
| rows = self.ints[index].float() | |
| scale = self.int8_scale[index].unsqueeze(-1).float() | |
| out = (rows * scale).to(self.compute_dtype) | |
| if self.embed_scale is not None: | |
| # Cast then multiply, in that order, because that is what | |
| # `Gemma4UnifiedTextScaledWordEmbedding.forward` does. | |
| out = out * self.embed_scale.to(self.compute_dtype) | |
| return out | |
| def extra_repr(self) -> str: | |
| scaled = "" if self.embed_scale is None else f", embed_scale={float(self.embed_scale):.4g}" | |
| return (f"{self.num_embeddings}, {self.embedding_dim}, kind=int8, " | |
| f"packed={self.packed_bytes() / 2**20:.1f} MiB{scaled}") | |
| def name_part(suffix: str, parts: dict) -> str: | |
| """The key in `parts` carrying `suffix`, or a miss the caller tolerates. | |
| `parts` comes either straight from a packer (keys are bare suffixes) or | |
| from a checkpoint read (keys are `name + suffix`), and both are worth | |
| supporting so a test does not have to fabricate tensor names. | |
| """ | |
| if suffix in parts: | |
| return suffix | |
| for key in parts: | |
| if key.endswith(suffix): | |
| return key | |
| return suffix | |
| def packed_linear_apply(reader, name: str, x: torch.Tensor, | |
| bias: torch.Tensor | None = None, rows: int = 512, | |
| dtype: torch.dtype = torch.float32) -> torch.Tensor: | |
| """`F.linear(x, W, bias)` for a packed W, `rows` output rows at a time. | |
| The two LTX aggregate tables are [4096, 188160] and [2048, 188160]. Reading | |
| one whole and casting it to float32 costs 3.08 GB, which is why | |
| `apply_aggregates` only runs after the encoder has been unloaded. Chunking | |
| is over *output* rows, so the contraction over `in_features` is untouched | |
| and the result is bit-identical to decoding the table in one piece. | |
| """ | |
| out_features = reader.row_count(name) | |
| pieces = [] | |
| for start in range(0, out_features, rows): | |
| stop = min(start + rows, out_features) | |
| weight = reader.get_tensor_rows(name, start, stop, dtype) | |
| piece = None if bias is None else bias[start:stop] | |
| pieces.append(torch.nn.functional.linear(x, weight, piece)) | |
| del weight | |
| return torch.cat(pieces, dim=-1) | |
| def cli_info(args) -> int: | |
| reader = PackedCheckpoint(args.packed) | |
| from collections import Counter | |
| counts = Counter(reader.kinds.values()) | |
| size = Path(args.packed).stat().st_size | |
| print(f"{args.packed}: {size / 1e9:.3f} GB, {dict(counts)}") | |
| print(f"negative-zero collapses at pack time: " | |
| f"{reader.metadata.get('negative_zero_collapses')}") | |
| for key in sorted(set(reader.metadata) - {"kinds"}): | |
| print(f" {key}: {reader.metadata[key][:100]}") | |
| return 0 | |
| def cli_verify(args) -> int: | |
| """Compare every tensor against a reference BF16 checkpoint, streaming.""" | |
| from safetensors import safe_open | |
| reader = PackedCheckpoint(args.packed) | |
| reference = safe_open(str(args.reference), framework="pt") | |
| names = set(reference.keys()) | |
| missing = sorted(set(reader.kinds) - names) | |
| extra = sorted(names - set(reader.kinds)) | |
| if missing or extra: | |
| raise SystemExit(f"tensor sets differ: missing {missing[:5]}, extra {extra[:5]}") | |
| worst = 0 | |
| for i, name in enumerate(sorted(names)): | |
| want = reference.get_tensor(name) | |
| got = reader.get_tensor(name, want.dtype) | |
| if not torch.equal(got, want): | |
| raise SystemExit(f"{name}: dequantized values differ from reference") | |
| worst += int((got.signbit() != want.signbit()).sum()) | |
| if i % 100 == 0: | |
| print(f" {i}/{len(names)} verified", flush=True) | |
| print(f"all {len(names)} tensors value-exact; {worst} zero-sign differences") | |
| return 0 | |
| def cli_emit_bf16(args) -> int: | |
| """Materialize a plain BF16 checkpoint a stock loader can read.""" | |
| from safetensors.torch import save_file | |
| reader = PackedCheckpoint(args.packed) | |
| tensors = {} | |
| for name in sorted(reader.kinds): | |
| tensors[name] = reader.get_tensor(name, torch.bfloat16) | |
| save_file(tensors, str(args.output)) | |
| print(f"wrote {args.output} ({Path(args.output).stat().st_size / 1e9:.2f} GB)") | |
| return 0 | |
| def main() -> int: | |
| parser = argparse.ArgumentParser() | |
| sub = parser.add_subparsers(dest="command", required=True) | |
| sub.add_parser("info").add_argument("packed") | |
| p = sub.add_parser("verify") | |
| p.add_argument("packed") | |
| p.add_argument("--reference", required=True) | |
| p = sub.add_parser("emit-bf16") | |
| p.add_argument("packed") | |
| p.add_argument("--output", required=True) | |
| args = parser.parse_args() | |
| return {"info": cli_info, "verify": cli_verify, | |
| "emit-bf16": cli_emit_bf16}[args.command](args) | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |