Spaces:
Paused
Paused
Download gemma/peft/_quantization_utils.py from Jit89/bmap-gemma-engine: direct link, hf CLI and curl.
- Browser
- Download file 8.23 kB
-
https://huggingface.co/spaces/Jit89/bmap-gemma-engine/resolve/main/gemma/peft/_quantization_utils.py
- Command line
-
hf download hf://spaces/Jit89/bmap-gemma-engine/gemma/peft/_quantization_utils.py
-
curl -L -o _quantization_utils.py https://huggingface.co/spaces/Jit89/bmap-gemma-engine/resolve/main/gemma/peft/_quantization_utils.py
8.23 kB
| # Copyright 2026 DeepMind Technologies Limited. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """params pre-processing.""" | |
| from collections.abc import Sequence | |
| import enum | |
| from typing import Any, TypeVar | |
| from etils import epy | |
| from flax.typing import Array # pylint: disable=g-importing-member | |
| import jax | |
| from jax import numpy as jnp | |
| PyTree = TypeVar('PyTree') | |
| _NON_ZERO_DIVISION_EPS = 1e-6 | |
| class QuantizationMethod(epy.StrEnum): | |
| """Quantization methods. | |
| Attributes: | |
| NONE: No quantization. | |
| INT4: 4 bits per-channel. | |
| Q4_0: 4 bits per-block. | |
| Q4_0_TRANSPOSE: 4 bits per-block (transpose first MLP layer). | |
| SFP8: 8 bits floating points. | |
| """ | |
| NONE = enum.auto() | |
| INT4 = enum.auto() | |
| INT8 = enum.auto() | |
| Q4_0 = enum.auto() | |
| Q4_0_TRANSPOSE = enum.auto() | |
| SFP8 = enum.auto() | |
| class QuantizationGranularity(epy.StrEnum): | |
| """Granularity of the quantization. | |
| Attributes: | |
| PER_TENSOR: scale the entire tensor. | |
| PER_CHANNEL: scale each channel independently. | |
| PER_BLOCK: scale each block independently. | |
| NOTE: a block is defined a contiguous sub sequence of a tensor, e.g. in | |
| 128x128 array would comprise 4x32 blocks of size 32. | |
| """ | |
| PER_TENSOR = enum.auto() | |
| PER_CHANNEL = enum.auto() | |
| PER_BLOCK = enum.auto() | |
| def quantize( | |
| params: PyTree, | |
| *, | |
| method: QuantizationMethod | str, | |
| checkpoint_kernel_key: str = 'w', | |
| in_place_keys: bool = False, | |
| ) -> PyTree: | |
| """Quantizes the given params. | |
| In ths API, we convert the elements of params in order to actually get | |
| quantized values. It is currently limited to INT$ per-channel weight | |
| quantization. | |
| Args: | |
| params: The params to quantize. | |
| method: The quantization method to use. | |
| checkpoint_kernel_key: The key of the kernel in the checkpoint (in | |
| pre-trained checkpoitns that is 'w'). | |
| in_place_keys: Whether to quantize the keys in place. | |
| Returns: | |
| The quantized params. | |
| """ | |
| method = QuantizationMethod(method) | |
| match method: | |
| case QuantizationMethod.NONE: | |
| return params | |
| case QuantizationMethod.INT4: | |
| quantization_func = uniform_quantize | |
| bitwidth = 4 | |
| dtype = jnp.int4 | |
| case QuantizationMethod.INT8: | |
| quantization_func = uniform_quantize | |
| bitwidth = 8 | |
| dtype = jnp.int8 | |
| case _: | |
| raise ValueError(f'Quantization method {method} is not yet supported.') | |
| def is_leaf(data: Any) -> bool: | |
| if isinstance(data, jax.Array): | |
| return True | |
| if checkpoint_kernel_key in data: | |
| return True | |
| if 'gating_einsum' in data or 'linear' in data: | |
| return True | |
| return False | |
| try: | |
| # get models dims to identify layer based on weight shape | |
| head_dim, d_model, _ = params['layer_0']['attn']['q_einsum'][ # pyrefly: ignore[unsupported-operation] | |
| checkpoint_kernel_key | |
| ].shape | |
| except KeyError: | |
| head_dim, d_model = -1, -1 | |
| def quantize_leaf(data: Any, key: str) -> Any: | |
| new_kernel, scales = quantization_func( | |
| data[key], | |
| bitwidth=bitwidth, | |
| granularity=QuantizationGranularity.PER_CHANNEL, | |
| dtype=dtype, # pyrefly: ignore[bad-argument-type] | |
| axis_to_reduce=_get_axis_to_reduce_from_weight_shape( # pyrefly: ignore[bad-argument-type] | |
| data[key].shape, head_dim=head_dim, d_model=d_model | |
| ), | |
| ) | |
| new_data = dict(data) | |
| del new_data[key] | |
| if in_place_keys: | |
| new_data['kernel'] = new_kernel | |
| new_data['scale'] = scales | |
| return new_data | |
| name = f'_IntEinsum_{key}' | |
| if key == checkpoint_kernel_key: | |
| name = '_IntEinsum_0' | |
| new_data[name] = {'kernel': new_kernel, 'scale': scales} | |
| return new_data | |
| def convert_leaf(data: Any) -> Any: | |
| if isinstance(data, jax.Array): | |
| return data | |
| if checkpoint_kernel_key in data: | |
| data = quantize_leaf(data, checkpoint_kernel_key) | |
| # This hack is required because the FeedForward layer call two different | |
| # Einsum with using `nn.share_scope`, so the two wrappers need a different | |
| # name. Weights are not stored under any kernel key and are isntead under | |
| # the following names. | |
| if 'gating_einsum' in data or 'linear' in data: | |
| data = quantize_leaf(data, 'gating_einsum') | |
| data = quantize_leaf(data, 'linear') | |
| return data | |
| quantized_params = jax.tree.map(convert_leaf, dict(params), is_leaf=is_leaf) # pyrefly: ignore[no-matching-overload] | |
| return quantized_params | |
| def uniform_quantize( | |
| x: Array, | |
| *, | |
| bitwidth: int, | |
| granularity: QuantizationGranularity, | |
| dtype: jnp.dtype = jnp.int4, # pyrefly: ignore[bad-function-definition] | |
| axis_to_reduce: int | None = None, | |
| ) -> tuple[Array, Array]: | |
| """Applies uniform quantization to the given array. | |
| We return the actually quantized array and the scale used (for de-quantization | |
| as a division, i.e. DQ(x) = x / scale).. | |
| Args: | |
| x: The array to quantize. | |
| bitwidth: The bitwidth of the quantization. | |
| granularity: The granularity of the quantization. | |
| dtype: The dtype to use for the quantization. | |
| axis_to_reduce: The axis to reduce over. | |
| Returns: | |
| The quantized array and the scale used. | |
| """ | |
| upper_bound = 2 ** (bitwidth - 1) - 1 | |
| lower_bound = -(2 ** (bitwidth - 1)) | |
| match granularity: | |
| case QuantizationGranularity.PER_TENSOR: | |
| max_ = jnp.max(jnp.abs(x)) | |
| scales = upper_bound * jnp.squeeze(jnp.array([1.0 / max_])) | |
| case QuantizationGranularity.PER_CHANNEL: | |
| if axis_to_reduce is None: | |
| max_ = reduce_max_all_but_one_axis(jnp.abs(x), axis=-1) | |
| else: | |
| max_ = jnp.max(jnp.abs(x), keepdims=True, axis=axis_to_reduce) | |
| scales = upper_bound * jnp.array(1.0 / max_) | |
| case _: | |
| raise ValueError(f'Unknown granularity: {granularity}') | |
| # apply scale | |
| x = x * scales | |
| # clip and round | |
| x = jnp.clip(jnp.round(x), lower_bound, upper_bound) | |
| return x.astype(dtype), jnp.maximum(scales, _NON_ZERO_DIVISION_EPS) | |
| def reduce_max_all_but_one_axis( | |
| x: Array, axis: int = 0, keepdims: bool = True | |
| ) -> Array: | |
| """Reduce the max of an array over all dimensions except one. | |
| This is useful to extract the max w.r.t. to the batch dimension for example. | |
| Args: | |
| x: The array to reduce | |
| axis: The axis not to reduce over | |
| keepdims: Whether to keep the reduced dimension | |
| Returns: | |
| The reduced array | |
| """ | |
| if axis < 0: | |
| axis += x.ndim | |
| dims = list(range(x.ndim)) | |
| dims.pop(axis) | |
| return jnp.max(x, axis=tuple(dims), keepdims=keepdims) | |
| def _replace_intermediate_keys(data, old_key, new_key): | |
| """Replaces intermediate keys in a nested dictionary. | |
| We use this to go from the simulated quantized nested params to the int4 | |
| inference nested params. | |
| Args: | |
| data: The nested dictionary. | |
| old_key: The key to replace. | |
| new_key: The new key. | |
| Returns: | |
| The modified dictionary. | |
| """ | |
| if isinstance(data, dict): | |
| return { | |
| k.replace(old_key, new_key): _replace_intermediate_keys( | |
| v, old_key, new_key | |
| ) | |
| for k, v in data.items() | |
| } | |
| else: | |
| return data | |
| def _get_axis_to_reduce_from_weight_shape( | |
| shape: Sequence[int], *, head_dim: int, d_model: int | |
| ) -> Sequence[int] | None: | |
| """Returns the axis to reduce over.""" | |
| if head_dim == -1 or d_model == -1: # no model dims available | |
| return None | |
| if len(shape) == 2: | |
| return (0,) | |
| if len(shape) == 3: | |
| if shape[0] == head_dim and shape[1] == d_model: # query einsum | |
| return (1,) | |
| if shape[0] == head_dim and shape[2] == d_model: # att out einsum | |
| return (0, 1) | |
| if shape[2] == d_model: # gate einsum | |
| return (2,) | |
| raise ValueError(f'Unsupported weight shape: {shape}') | |
| if len(shape) == 4: | |
| return (2,) | |
| raise ValueError(f'Unsupported weight shape: {shape}') | |