Coverage for transformer_lens/utilities/quantization.py: 100%
50 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-01 16:23 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-01 16:23 +0000
1"""Guards for weight-space code paths that cannot read quantized weights.
3TransformerLens supports quantized *forward* passes: the wrapped HF module
4dequantizes internally, and the bridge's forward paths deliberately skip
5non-floating-point parameters when picking a compute dtype. Those paths must
6keep working.
8What does not work is reading a quantized ``.weight`` and doing arithmetic on it
9directly — reshaping it into per-head matrices, slicing a fused projection,
10folding LayerNorm into it. There the storage is packed (bitsandbytes 4-bit keeps
11a ``[N, 1]`` uint8 buffer), split from its scales (FP8 keeps a separate
12``weight_scale_inv``), or not a tensor at all (MXFP4 wraps a triton-kernels
13object). Slicing those yields plausible-looking garbage rather than an error,
14which is the failure mode this module exists to prevent.
15"""
17from __future__ import annotations
19from typing import Any, Optional
21import torch
24def unreadable_weight_reason(weight: Any) -> Optional[str]:
25 """Why ``weight`` cannot be read as a plain matrix (a fragment completing
26 "... cannot be read because <reason>"), or None if it can.
28 Readable = exactly {fp16, bf16, fp32, fp64}: every 1-byte torch dtype is
29 integer storage or a scale-less narrow float.
30 """
31 if not isinstance(weight, torch.Tensor):
32 # MXFP4 and similar hand back a wrapper object holding the packed
33 # payload plus scales. It is often *named* Tensor, which makes the
34 # resulting TypeError look like a torch bug — hence the qualified name,
35 # which is the only thing distinguishing it from the real one.
36 cls = type(weight)
37 return f"it is a {cls.__module__}.{cls.__qualname__}, not a torch.Tensor"
38 if weight.device.type == "meta":
39 return "it is on the meta device, so no values have been materialized"
40 if not weight.dtype.is_floating_point:
41 # bitsandbytes int8/4-bit, GPTQ/AWQ int32, BitNet packed uint8. Note
42 # that bnb's Params4bit IS a torch.Tensor subclass, so only the dtype
43 # distinguishes it — and its shape is the packed [N, 1], not [out, in].
44 return f"its dtype is {weight.dtype}, which is packed integer storage"
45 if weight.dtype.itemsize < 2:
46 # float8_e4m3fn and friends report is_floating_point=True, so a plain
47 # float check lets them through. They are the only family that slices
48 # and even survives nn.Parameter() silently, so omitting this branch
49 # leaves the worst case uncaught.
50 return (
51 f"its dtype is {weight.dtype}, a narrow float whose values are "
52 "meaningless without the separate scale tensor stored beside them "
53 "(e.g. weight_scale_inv)"
54 )
55 return None
58def _is_meta(weight: Any) -> bool:
59 return isinstance(weight, torch.Tensor) and weight.device.type == "meta"
62def quantization_method(config: Any) -> Optional[str]:
63 """The ``quant_method`` declared on an HF config, or None if unquantized.
65 Accepts a ``PretrainedConfig`` or its ``to_dict()`` form, and tolerates the
66 nested ``quantization_config`` being either shape — both appear in the wild,
67 depending on whether the config was loaded or round-tripped through JSON.
68 """
69 if config is None:
70 return None
71 if isinstance(config, dict):
72 quant_config = config.get("quantization_config")
73 else:
74 quant_config = getattr(config, "quantization_config", None)
75 if quant_config is None:
76 return None
77 if isinstance(quant_config, dict):
78 method = quant_config.get("quant_method")
79 else:
80 method = getattr(quant_config, "quant_method", None)
81 # HF stores this as a str or a str-valued QuantizationMethod enum; require
82 # that rather than str()-ing whatever turned up, or a stub object's repr
83 # ends up quoted back at the user as a method name.
84 method = getattr(method, "value", method)
85 return method if isinstance(method, str) else None
88def describe_quantization(owner: Any) -> str:
89 """Best-effort name for how ``owner``'s weights are quantized.
91 Resolution order: the HF config's declared ``quant_method``, then the
92 weight's class name (bitsandbytes subclasses are identifiable that way),
93 then a generic fallback.
94 """
95 for holder in (owner, getattr(owner, "original_component", None)):
96 method = quantization_method(getattr(holder, "config", None))
97 if method is not None:
98 return method
99 weight = getattr(owner, "weight", None)
100 if weight is not None:
101 # Exclude the two uninformative names rather than nn.Parameter itself:
102 # bitsandbytes' Params4bit and Int8Params ARE Parameter subclasses, so
103 # an isinstance test excluded exactly the classes this identifies.
104 # transformers keys off the same class names (integrations/bitsandbytes).
105 name = type(weight).__name__
106 if name not in ("Tensor", "Parameter"):
107 return name
108 return "an unknown quantization"
111_GENERIC_REMEDY = (
112 "Weight-space operations need dequantized weights — reload the model "
113 "without a quantization_config (or with dequantize enabled). Quantized "
114 "*forward* passes remain supported."
115)
118def require_readable_weight(
119 weight: Any, *, operation: str, owner: Any = None, remedy: Optional[str] = None
120) -> torch.Tensor:
121 """Return ``weight`` if it can be read as a plain matrix, else raise loudly.
123 ``operation`` completes "TransformerLens cannot <operation> because ...";
124 ``remedy`` replaces the generic advice — phrase it to hold for ANY
125 quantization, since the caller cannot know which one it caught.
126 """
127 reason = unreadable_weight_reason(weight)
128 if reason is None:
129 assert isinstance(weight, torch.Tensor)
130 return weight
131 if _is_meta(weight):
132 # Not a quantization problem — naming one here would send the reader
133 # after the wrong cause. This is an offloaded or never-materialized load.
134 raise NotImplementedError(
135 f"TransformerLens cannot {operation} because {reason}. Load the "
136 "model with real weights (no meta-device or disk offload) before "
137 "reading them."
138 )
139 method = describe_quantization(owner) if owner is not None else "an unknown quantization"
140 raise NotImplementedError(
141 f"TransformerLens cannot {operation} because {reason} "
142 f"(quantization: {method}). {remedy or _GENERIC_REMEDY}"
143 )