LTX-2.5-Text-Encoder-4bit-8GB / ltx_packed_codec.py
topabaem's picture
Check for kernels before loading, not after
b0a01d0 verified
Raw
History Blame Contribute Delete
40.7 kB
"""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))
# --- inlined from ltx_fake_quantize -----------------------------------------
#: The eight magnitudes E2M1 can represent, and the midpoints between them that
#: decide which one a value rounds to.
E2M1_LEVELS = (0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0)
E2M1_MIDPOINTS = (0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5.0)
NVFP4_GROUP = 16
#: E4M3's largest finite value; the per-tensor scale is chosen so the per-group
#: scales divided by it land inside this range.
E4M3_MAX = 448.0
GPU_TENSOR_LIMIT = 1_000_000_000
# --- inlined from ltx_prompt_embedding_gate ---------------------------------
#: Lightricks re-exported the encoder under the naming Google used before the
#: `gemma4_unified` port landed in `transformers`. Without these rules
#: `from_pretrained` reports every checkpoint tensor UNEXPECTED and every
#: parameter MISSING, and hands back a randomly initialised model that looks
#: like it loaded.
LTX_RENAMES = [
(r"^model\.", "language_model."),
(r"^vision_model\.", "embed_vision."),
(r"^audio_projector\.", "embed_audio."),
(r"^multi_modal_projector\.embedding_projection",
"embed_vision.multimodal_embedder.embedding_projection"),
]
def register_ltx_renames() -> None:
from transformers.conversion_mapping import (
get_checkpoint_conversion_mapping,
register_checkpoint_conversion_mapping,
)
from transformers.core_model_loading import WeightRenaming
existing = get_checkpoint_conversion_mapping("gemma4_unified") or []
rules = [WeightRenaming(source_patterns=s, target_patterns=t)
for s, t in LTX_RENAMES]
register_checkpoint_conversion_mapping(
"gemma4_unified", rules + existing, overwrite=True)
# ----------------------------------------------------------------------------
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 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
@property
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())
@property
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())