quantbench-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ # Copyright 2026 Prajwal7-2002
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models.
16
+
17
+ Mathematical Foundation:
18
+ LLM activations exhibit severe outlier channels whose magnitudes can be 100x
19
+ larger than typical channels, making activation quantization to INT8 prone to
20
+ catastrophic precision loss under per-tensor or per-token absmax scaling.
21
+ Conversely, weights exhibit smooth channel distributions and are easy to quantize.
22
+
23
+ SmoothQuant introduces a mathematically invariant per-channel migration scale s:
24
+ Y = X * W = (X * diag(s)^-1) * (diag(s) * W) = X_hat * W_hat
25
+
26
+ The scale vector s in R^C is chosen to balance the maximum dynamic ranges:
27
+ s_j = (max(|X_j|)^alpha) / (max(|W_j|)^(1 - alpha))
28
+
29
+ where alpha in [0, 1] controls migration strength:
30
+ - alpha = 0.5: Symmetrically divides difficulty between activations and weights.
31
+ - alpha = 1.0: Migrates all difficulty to the weights (activations become easy to quantize).
32
+ - alpha = 0.0: Preserves weights completely (no difficulty migration).
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from typing import Optional, Tuple
38
+
39
+ import torch
40
+ import torch.nn as nn
41
+
42
+
43
+ def compute_smoothquant_scale(
44
+ act_max: torch.Tensor,
45
+ weight_max: torch.Tensor,
46
+ alpha: float = 0.5,
47
+ eps: float = 1e-5,
48
+ ) -> torch.Tensor:
49
+ """Compute the per-channel SmoothQuant migration scale vector.
50
+
51
+ Args:
52
+ act_max: Tensor of shape (C,) representing the maximum absolute activation
53
+ magnitude per input channel across calibration tokens.
54
+ weight_max: Tensor of shape (C,) representing the maximum absolute weight
55
+ magnitude per input channel: max_i |W_{i, j}|.
56
+ alpha: Migration strength hyperparameter in [0.0, 1.0].
57
+ eps: Small epsilon to prevent division by zero or degenerate scales.
58
+
59
+ Returns:
60
+ Tensor of shape (C,) containing per-channel migration scales s_j.
61
+ """
62
+ if not (0.0 <= alpha <= 1.0):
63
+ raise ValueError(f"alpha must be in [0.0, 1.0], got {alpha}")
64
+
65
+ act_max = act_max.clamp(min=eps)
66
+ weight_max = weight_max.clamp(min=eps)
67
+
68
+ # s_j = (s_X^alpha) / (s_W^(1 - alpha))
69
+ numerator = act_max.pow(alpha)
70
+ denominator = weight_max.pow(1.0 - alpha)
71
+ scale = (numerator / denominator).clamp(min=eps)
72
+ return scale
73
+
74
+
75
+ def extract_activation_channel_scales(
76
+ module: nn.Module,
77
+ calibration_inputs: list[torch.Tensor] | torch.Tensor,
78
+ ) -> torch.Tensor:
79
+ """Profile activation outlier channel scales across calibration data.
80
+
81
+ Args:
82
+ module: Neural network module (e.g. nn.Linear).
83
+ calibration_inputs: A tensor or list of tensors of shape (B, S, C_in) or (N, C_in).
84
+
85
+ Returns:
86
+ Tensor of shape (C_in,) containing per-channel max absolute activation values.
87
+ """
88
+ if isinstance(calibration_inputs, torch.Tensor):
89
+ calibration_inputs = [calibration_inputs]
90
+
91
+ channel_max: Optional[torch.Tensor] = None
92
+
93
+ for batch in calibration_inputs:
94
+ # Reshape to (N_tokens, C_in)
95
+ flat = batch.detach().view(-1, batch.shape[-1]).abs()
96
+ batch_max = flat.max(dim=0).values
97
+ if channel_max is None:
98
+ channel_max = batch_max
99
+ else:
100
+ channel_max = torch.maximum(channel_max, batch_max)
101
+
102
+ if channel_max is None:
103
+ raise ValueError("calibration_inputs cannot be empty.")
104
+
105
+ return channel_max
106
+
107
+
108
+ def smooth_linear_weights(
109
+ weight: torch.Tensor,
110
+ act_scales: torch.Tensor,
111
+ alpha: float = 0.5,
112
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
113
+ """Apply SmoothQuant per-channel migration to weight tensor.
114
+
115
+ Args:
116
+ weight: Float tensor of shape (out_features, in_features).
117
+ act_scales: Per-channel activation max tensor of shape (in_features,).
118
+ alpha: Migration factor in [0.0, 1.0].
119
+
120
+ Returns:
121
+ Tuple of (smoothed_weight, migration_scale):
122
+ - smoothed_weight: W * diag(s) of shape (out_features, in_features).
123
+ - migration_scale: s of shape (in_features,).
124
+ """
125
+ # W shape: (out_features, in_features)
126
+ # Channel max across output rows: max_i |W_{i, j}| -> shape (in_features,)
127
+ weight_max = weight.abs().max(dim=0).values
128
+ scale = compute_smoothquant_scale(act_scales, weight_max, alpha=alpha)
129
+
130
+ # W_hat = W * diag(s) -> broadcasting along dim 1
131
+ smoothed_weight = weight * scale.unsqueeze(0)
132
+ return smoothed_weight, scale
@@ -0,0 +1,115 @@
1
+ """Group-wise (Block-wise) Quantization Primitives.
2
+
3
+ Partitions tensors into independent contiguous blocks of size G (e.g. 32, 64, 128)
4
+ to isolate channel outliers and preserve precision across wide transformer layers.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional, Tuple
8
+
9
+ import torch
10
+
11
+ from quantbench.quant.affine import (
12
+ dequantize_absmax,
13
+ dequantize_affine,
14
+ quantize_absmax,
15
+ quantize_affine,
16
+ )
17
+
18
+
19
+ def quantize_blockwise(
20
+ x: torch.Tensor,
21
+ group_size: int = 128,
22
+ num_bits: int = 4,
23
+ symmetric: bool = True,
24
+ signed: bool = False,
25
+ ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor], Dict[str, Any]]:
26
+ """Quantize a tensor into independent group-wise blocks.
27
+
28
+ Divides flattened tensor into blocks of size `group_size`. If the tensor element
29
+ count is not an exact multiple of `group_size`, pads with zeros.
30
+
31
+ Args:
32
+ x: Input tensor of arbitrary shape.
33
+ group_size: Number of consecutive elements per block (32, 64, 128).
34
+ num_bits: Bit precision per element (e.g. 4 or 8).
35
+ symmetric: If True, uses symmetric AbsMax per block (zero_point = 0).
36
+ If False, uses asymmetric affine (scale and zero_point).
37
+ signed: Whether integer values are signed (only used when symmetric=False).
38
+
39
+ Returns:
40
+ Tuple of (q_blocks, scales, zero_points, metadata_dict).
41
+ """
42
+ orig_shape = tuple(x.shape)
43
+ flat_x = x.contiguous().view(-1)
44
+ n_elements = flat_x.numel()
45
+
46
+ # Calculate padding if necessary
47
+ remainder = n_elements % group_size
48
+ pad_len = (group_size - remainder) % group_size
49
+ if pad_len > 0:
50
+ flat_x = torch.nn.functional.pad(flat_x, (0, pad_len), value=0.0)
51
+
52
+ # Reshape into blocks: [num_blocks, group_size]
53
+ blocked_x = flat_x.view(-1, group_size)
54
+
55
+ if symmetric:
56
+ q_blocks, scales = quantize_absmax(
57
+ blocked_x,
58
+ num_bits=num_bits,
59
+ dim=1,
60
+ symmetric_range=True,
61
+ )
62
+ zero_points = None
63
+ else:
64
+ q_blocks, scales, zero_points = quantize_affine(
65
+ blocked_x,
66
+ num_bits=num_bits,
67
+ dim=1,
68
+ signed=signed,
69
+ )
70
+
71
+ meta = {
72
+ "original_shape": orig_shape,
73
+ "n_elements": n_elements,
74
+ "pad_len": pad_len,
75
+ "group_size": group_size,
76
+ "num_bits": num_bits,
77
+ "symmetric": symmetric,
78
+ "dtype": x.dtype,
79
+ "device": x.device,
80
+ }
81
+
82
+ return q_blocks, scales, zero_points, meta
83
+
84
+
85
+ def dequantize_blockwise(
86
+ q_blocks: torch.Tensor,
87
+ scales: torch.Tensor,
88
+ zero_points: Optional[torch.Tensor],
89
+ meta: Dict[str, Any],
90
+ ) -> torch.Tensor:
91
+ """Reconstruct an original floating-point tensor from block-wise quantized representations.
92
+
93
+ Args:
94
+ q_blocks: Quantized integer blocks of shape [num_blocks, group_size].
95
+ scales: Scale factors of shape [num_blocks, 1].
96
+ zero_points: Optional zero-points of shape [num_blocks, 1].
97
+ meta: Metadata dictionary returned by quantize_blockwise.
98
+
99
+ Returns:
100
+ Reconstructed tensor matching original shape and device.
101
+ """
102
+ if meta["symmetric"]:
103
+ x_recon_blocked = dequantize_absmax(q_blocks, scales)
104
+ else:
105
+ assert zero_points is not None, (
106
+ "zero_points required for asymmetric blockwise dequantization"
107
+ )
108
+ x_recon_blocked = dequantize_affine(q_blocks, scales, zero_points)
109
+
110
+ flat_recon = x_recon_blocked.view(-1)
111
+ # Remove padding if added during quantization
112
+ if meta["pad_len"] > 0:
113
+ flat_recon = flat_recon[: meta["n_elements"]]
114
+
115
+ return flat_recon.view(meta["original_shape"]).to(meta["dtype"])
@@ -0,0 +1,166 @@
1
+ """OCP FP8 (E4M3FN and E5M2) Emulation and Simulation Primitives.
2
+
3
+ Implements bit-exact simulation of the two standardized 8-bit floating-point formats:
4
+ 1. E4M3FN: 1 sign, 4 exponent, 3 mantissa (bias 7, max ±448.0, no infinity, higher precision).
5
+ 2. E5M2: 1 sign, 5 exponent, 2 mantissa (bias 15, max ±57344.0, IEEE inf/nan, wider dynamic range).
6
+ """
7
+
8
+ from enum import Enum
9
+ from typing import Optional, Tuple
10
+
11
+ import torch
12
+
13
+
14
+ class FP8Format(str, Enum):
15
+ """Standardized Open Compute Project (OCP) 8-bit floating point formats."""
16
+
17
+ E4M3 = "e4m3fn"
18
+ E5M2 = "e5m2"
19
+
20
+
21
+ FP8_MAX_VALUES = {
22
+ FP8Format.E4M3: 448.0,
23
+ FP8Format.E5M2: 57344.0,
24
+ }
25
+
26
+
27
+ def _emulate_fp8(
28
+ x: torch.Tensor,
29
+ exp_bits: int,
30
+ mantissa_bits: int,
31
+ bias: int,
32
+ max_val: float,
33
+ has_inf: bool = False,
34
+ ) -> torch.Tensor:
35
+ """Analytical bit-level emulator for floating-point formats with arbitrary bitwidths.
36
+
37
+ Simulates the exact IEEE / OCP quantization, subnormal behavior, and saturation.
38
+ """
39
+ sign = torch.sign(x)
40
+ abs_x = torch.abs(x)
41
+
42
+ # Saturate / clamp to max representable value
43
+ abs_x = torch.clamp(abs_x, max=max_val)
44
+
45
+ # Handle zeros
46
+ is_zero = abs_x == 0.0
47
+
48
+ # Calculate base-2 exponent
49
+ # e = floor(log2(x))
50
+ log2_x = torch.log2(torch.clamp(abs_x, min=1e-30))
51
+ exp = torch.floor(log2_x)
52
+
53
+ # Minimum normal exponent
54
+ min_exp = 1 - bias
55
+ max_exp = (1 << exp_bits) - 1 - bias
56
+
57
+ # Separate normals from subnormals
58
+ is_subnormal = exp < min_exp
59
+ exp = torch.clamp(exp, min=min_exp, max=max_exp)
60
+
61
+ # Mantissa quantization step size
62
+ # For normals: step = 2^(exp - mantissa_bits)
63
+ # For subnormals: step = 2^(min_exp - mantissa_bits)
64
+ eff_exp = torch.where(is_subnormal, torch.full_like(exp, min_exp), exp)
65
+ step = torch.pow(2.0, eff_exp - mantissa_bits)
66
+
67
+ # Discretize mantissa via round-to-nearest
68
+ q_abs = torch.round(abs_x / step) * step
69
+
70
+ # Re-apply clamp
71
+ q_abs = torch.clamp(q_abs, max=max_val)
72
+
73
+ # Zero out true zeros
74
+ q_abs = torch.where(is_zero, torch.zeros_like(q_abs), q_abs)
75
+
76
+ return sign * q_abs
77
+
78
+
79
+ def quantize_fp8(
80
+ x: torch.Tensor,
81
+ format: FP8Format = FP8Format.E4M3,
82
+ scale: Optional[torch.Tensor] = None,
83
+ eps: float = 1e-8,
84
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
85
+ """Quantize floating-point tensor to FP8 with dynamic or user-specified scaling.
86
+
87
+ Computes:
88
+ scale = max(|x|) / max_representable_fp8 (if scale is None)
89
+ scaled_x = x / scale
90
+ q_fp8 = quantize(scaled_x)
91
+
92
+ Args:
93
+ x: Input tensor.
94
+ format: FP8Format.E4M3 or FP8Format.E5M2.
95
+ scale: Optional precomputed scale factor tensor.
96
+ eps: Small value to avoid division by zero.
97
+
98
+ Returns:
99
+ Tuple of (quantized_fp8_tensor, scale).
100
+ """
101
+ max_repr = FP8_MAX_VALUES[format]
102
+
103
+ if scale is None:
104
+ max_val = x.abs().max()
105
+ scale = max_val / float(max_repr)
106
+ scale = torch.clamp(scale, min=eps)
107
+
108
+ scaled_x = x / scale
109
+
110
+ # Check if native torch float8 dtype is supported and available
111
+ native_dtype = None
112
+ if hasattr(torch, "float8_e4m3fn") and format == FP8Format.E4M3:
113
+ native_dtype = torch.float8_e4m3fn
114
+ elif hasattr(torch, "float8_e5m2") and format == FP8Format.E5M2:
115
+ native_dtype = torch.float8_e5m2
116
+
117
+ if native_dtype is not None:
118
+ try:
119
+ # Native PyTorch conversion
120
+ q = scaled_x.to(native_dtype)
121
+ return q, scale
122
+ except Exception:
123
+ pass
124
+
125
+ # Analytical fallback emulation
126
+ if format == FP8Format.E4M3:
127
+ q = _emulate_fp8(
128
+ scaled_x,
129
+ exp_bits=4,
130
+ mantissa_bits=3,
131
+ bias=7,
132
+ max_val=448.0,
133
+ has_inf=False,
134
+ )
135
+ else:
136
+ q = _emulate_fp8(
137
+ scaled_x,
138
+ exp_bits=5,
139
+ mantissa_bits=2,
140
+ bias=15,
141
+ max_val=57344.0,
142
+ has_inf=True,
143
+ )
144
+
145
+ return q, scale
146
+
147
+
148
+ def dequantize_fp8(
149
+ q_fp8: torch.Tensor,
150
+ scale: torch.Tensor,
151
+ target_dtype: torch.dtype = torch.float32,
152
+ ) -> torch.Tensor:
153
+ """Dequantize an FP8 tensor back to target floating-point precision.
154
+
155
+ Computes:
156
+ x_hat = q_fp8 * scale
157
+
158
+ Args:
159
+ q_fp8: Quantized FP8 tensor.
160
+ scale: Scale factor tensor.
161
+ target_dtype: Desired output floating-point precision (e.g. float16, float32).
162
+
163
+ Returns:
164
+ Dequantized tensor.
165
+ """
166
+ return q_fp8.to(target_dtype) * scale.to(target_dtype)
@@ -0,0 +1,27 @@
1
+ """Bit-packing runtimes for sub-byte quantization formats.
2
+
3
+ Provides vectorized bit-shift packing and unpacking for:
4
+ - INT4 (4-bit integers: unsigned [0, 15] or signed [-8, 7]) -> 2 values per uint8 byte.
5
+ - Ternary (BitNet 1.58b: {-1, 0, +1}) -> 4 values per uint8 byte.
6
+ - 2:4 Structured Sparsity Metadata -> two 2-bit indices per 4-bit nibble.
7
+ """
8
+
9
+ from quantbench.quant.packing.int4_pack import pack_int4, unpack_int4
10
+ from quantbench.quant.packing.sparse_pack import (
11
+ decompress_2_4,
12
+ pack_2_4_indices,
13
+ prune_and_compress_2_4,
14
+ unpack_2_4_indices,
15
+ )
16
+ from quantbench.quant.packing.ternary_pack import pack_ternary, unpack_ternary
17
+
18
+ __all__ = [
19
+ "pack_int4",
20
+ "unpack_int4",
21
+ "pack_ternary",
22
+ "unpack_ternary",
23
+ "pack_2_4_indices",
24
+ "unpack_2_4_indices",
25
+ "prune_and_compress_2_4",
26
+ "decompress_2_4",
27
+ ]
@@ -0,0 +1,169 @@
1
+ """Vectorized INT4 bit-packing runtime for sub-byte quantization.
2
+
3
+ Packs two 4-bit values (either unsigned [0, 15] or signed [-8, 7]) into a single uint8
4
+ byte container, delivering a 4x memory footprint reduction over FP16 and 2x over INT8.
5
+
6
+ Bit layout within each packed uint8 byte:
7
+ - Bits [0:3] (Low nibble): Even element index (2k)
8
+ - Bits [4:7] (High nibble): Odd element index (2k + 1)
9
+
10
+ Mathematical mapping:
11
+ byte = (v_even & 0x0F) | ((v_odd & 0x0F) << 4)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import torch
17
+
18
+
19
+ def pack_nibbles(low: int, high: int) -> int:
20
+ """Pack two 4-bit integer scalars into a single 8-bit unsigned byte.
21
+
22
+ Args:
23
+ low: Integer value for low nibble (bits 0..3), range [0, 15] or [-8, 7].
24
+ high: Integer value for high nibble (bits 4..7), range [0, 15] or [-8, 7].
25
+
26
+ Returns:
27
+ Unsigned 8-bit integer in range [0, 255].
28
+ """
29
+ u_low = low & 0x0F
30
+ u_high = high & 0x0F
31
+ return (u_low) | (u_high << 4)
32
+
33
+
34
+ def unpack_nibbles(byte: int, signed: bool = False) -> tuple[int, int]:
35
+ """Unpack a single 8-bit unsigned byte into two 4-bit integer scalars.
36
+
37
+ Args:
38
+ byte: Unsigned 8-bit integer in range [0, 255].
39
+ signed: If True, sign-extends 4-bit values into signed range [-8, 7].
40
+ If False, returns unsigned values in range [0, 15].
41
+
42
+ Returns:
43
+ Tuple of (low_nibble, high_nibble).
44
+ """
45
+ low = byte & 0x0F
46
+ high = (byte >> 4) & 0x0F
47
+ if signed:
48
+ if low >= 8:
49
+ low -= 16
50
+ if high >= 8:
51
+ high -= 16
52
+ return low, high
53
+
54
+
55
+ def pack_int4(
56
+ tensor: torch.Tensor,
57
+ signed: bool = False,
58
+ dim: int = -1,
59
+ ) -> tuple[torch.Tensor, int]:
60
+ """Vectorized bit-shift packing of INT4 values into a uint8 tensor.
61
+
62
+ Args:
63
+ tensor: Integer PyTorch tensor containing 4-bit values.
64
+ If signed=False: values must be in [0, 15].
65
+ If signed=True: values must be in [-8, 7].
66
+ signed: Whether the input values are signed two's complement.
67
+ dim: Dimension along which to pack adjacent pairs. Defaults to last dimension (-1).
68
+
69
+ Returns:
70
+ Tuple of (packed_uint8_tensor, original_dim_length).
71
+ The packed tensor will have size ceil(original_length / 2) along `dim`.
72
+ """
73
+ if not tensor.dtype.is_floating_point and tensor.dtype != torch.bool:
74
+ t_int = tensor.to(torch.int32)
75
+ else:
76
+ t_int = tensor.round().to(torch.int32)
77
+
78
+ # Validate bounds
79
+ if signed:
80
+ if (t_int < -8).any() or (t_int > 7).any():
81
+ raise ValueError("Signed INT4 values must fall strictly within range [-8, 7].")
82
+ else:
83
+ if (t_int < 0).any() or (t_int > 15).any():
84
+ raise ValueError("Unsigned INT4 values must fall strictly within range [0, 15].")
85
+
86
+ # Normalize dim
87
+ dim = dim if dim >= 0 else tensor.ndim + dim
88
+ orig_len = tensor.shape[dim]
89
+
90
+ # Convert to 4-bit unsigned representation
91
+ u4 = t_int & 0x0F
92
+
93
+ # Pad by 1 if odd length along dim
94
+ if orig_len % 2 != 0:
95
+ pad_shape = list(u4.shape)
96
+ pad_shape[dim] = 1
97
+ pad_tensor = torch.zeros(pad_shape, dtype=u4.dtype, device=u4.device)
98
+ u4 = torch.cat([u4, pad_tensor], dim=dim)
99
+
100
+ # Slice into even and odd indices
101
+ # We move target dim to last position for easy contiguous slicing
102
+ if dim != u4.ndim - 1:
103
+ u4 = u4.transpose(dim, -1)
104
+
105
+ low = u4[..., 0::2]
106
+ high = u4[..., 1::2]
107
+
108
+ packed = (low & 0x0F) | ((high & 0x0F) << 4)
109
+ packed_uint8 = packed.to(torch.uint8)
110
+
111
+ if dim != tensor.ndim - 1:
112
+ packed_uint8 = packed_uint8.transpose(dim, -1)
113
+
114
+ return packed_uint8, orig_len
115
+
116
+
117
+ def unpack_int4(
118
+ packed: torch.Tensor,
119
+ original_len: int | None = None,
120
+ signed: bool = False,
121
+ dim: int = -1,
122
+ ) -> torch.Tensor:
123
+ """Vectorized unpacking of a uint8 packed tensor back into INT4 values.
124
+
125
+ Args:
126
+ packed: Tensor of dtype torch.uint8 containing packed pairs.
127
+ original_len: Optional original dimension length before packing.
128
+ Used to remove padding if original length was odd.
129
+ signed: If True, sign-extends values to signed range [-8, 7].
130
+ If False, returns unsigned values in [0, 15].
131
+ dim: Dimension along which elements were packed. Defaults to -1.
132
+
133
+ Returns:
134
+ Unpacked tensor with dtype torch.int8 (if signed) or torch.uint8 (if unsigned),
135
+ restored to the original length along `dim`.
136
+ """
137
+ if packed.dtype != torch.uint8:
138
+ raise TypeError(f"Packed tensor must have dtype torch.uint8, got {packed.dtype}.")
139
+
140
+ dim = dim if dim >= 0 else packed.ndim + dim
141
+ if dim != packed.ndim - 1:
142
+ packed = packed.transpose(dim, -1)
143
+
144
+ # Extract low nibble and high nibble
145
+ p32 = packed.to(torch.int32)
146
+ low = p32 & 0x0F
147
+ high = (p32 >> 4) & 0x0F
148
+
149
+ # Interleave low and high along a new sub-dimension
150
+ # Shape becomes (*batch_dims, packed_len, 2) -> flatten to (*batch_dims, packed_len * 2)
151
+ interleaved = torch.stack([low, high], dim=-1)
152
+ flat_shape = list(interleaved.shape[:-2]) + [interleaved.shape[-2] * 2]
153
+ unpacked = interleaved.view(flat_shape)
154
+
155
+ # Truncate padding if original_len is specified
156
+ if original_len is not None:
157
+ unpacked = unpacked[..., :original_len]
158
+
159
+ if signed:
160
+ # Values in 8..15 have the sign bit set; subtract 16
161
+ unpacked = torch.where(unpacked >= 8, unpacked - 16, unpacked)
162
+ unpacked = unpacked.to(torch.int8)
163
+ else:
164
+ unpacked = unpacked.to(torch.uint8)
165
+
166
+ if dim != packed.ndim - 1:
167
+ unpacked = unpacked.transpose(dim, -1)
168
+
169
+ return unpacked