mirror of
https://github.com/NVIDIA/Model-Optimizer.git
synced 2026-11-04 08:27:53 +08:00
313 lines
12 KiB
Python
313 lines
12 KiB
Python
# Inspired by https://github.com/ELS-RD/transformer-deploy/blob/6b88e24ade6ce199e825adc0477b28a07f51f17d/src/transformer_deploy/QDQModels/ast_operator_patch.py
|
|
|
|
# Apache License
|
|
# Copyright 2022, Lefebvre Dalloz Services
|
|
|
|
# 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.
|
|
|
|
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# 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.
|
|
|
|
"""Support quantization for KV Cache in attention layers."""
|
|
|
|
import ast
|
|
import inspect
|
|
import tempfile
|
|
import types
|
|
from warnings import warn
|
|
|
|
from ..conversion import register
|
|
from ..nn import TensorQuantizer
|
|
|
|
__all__ = ["register_attention_for_kv_quant"]
|
|
|
|
|
|
def register_attention_for_kv_quant(attention_cls: type) -> bool:
|
|
"""Register attention layer for quantization of KV Cache.
|
|
|
|
Generate a quantized version of the attention class on the fly,
|
|
and register it with the original class for quantization.
|
|
"""
|
|
source_code = inspect.getsource(attention_cls)
|
|
model_module = inspect.getmodule(attention_cls)
|
|
head = ast.parse(source_code)
|
|
|
|
bmm_ops = ("matmul", "bmm", "baddbmm")
|
|
sdpa_ops = ("scaled_dot_product_attention",)
|
|
|
|
def is_bmm(node):
|
|
return (
|
|
isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Attribute)
|
|
and node.func.attr in bmm_ops
|
|
)
|
|
|
|
def is_sdpa(node):
|
|
return (
|
|
isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Attribute)
|
|
and node.func.attr in sdpa_ops
|
|
)
|
|
|
|
def is_bin_matmul(node):
|
|
return isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult)
|
|
|
|
def patch(node, quantizer_names, transpose=False):
|
|
for index, quantizer_name in enumerate(quantizer_names):
|
|
if quantizer_name is None:
|
|
continue
|
|
arg = node.args[index]
|
|
|
|
if not transpose:
|
|
node.args[index] = ast.Call(
|
|
func=ast.Attribute(
|
|
value=ast.Name(id="self", ctx=ast.Load()),
|
|
attr=quantizer_name,
|
|
ctx=ast.Load(),
|
|
),
|
|
args=[arg],
|
|
keywords=[],
|
|
)
|
|
else:
|
|
node.args[index] = ast.Call(
|
|
func=ast.Attribute(
|
|
value=ast.Call(
|
|
func=ast.Attribute(
|
|
value=ast.Name(id="self", ctx=ast.Load()),
|
|
attr=quantizer_name,
|
|
ctx=ast.Load(),
|
|
),
|
|
args=[
|
|
ast.Call(
|
|
func=ast.Attribute(value=arg, attr="transpose", ctx=ast.Load()),
|
|
args=[ast.Constant(value=-1), ast.Constant(value=-2)],
|
|
keywords=[],
|
|
)
|
|
],
|
|
keywords=[],
|
|
),
|
|
attr="transpose",
|
|
ctx=ast.Load(),
|
|
),
|
|
args=[ast.Constant(value=-1), ast.Constant(value=-2)],
|
|
keywords=[],
|
|
)
|
|
|
|
def patch_binop(node, quantizer_names, transpose=False):
|
|
assert len(quantizer_names) == 2
|
|
if quantizer_names[0] is not None:
|
|
node.left = ast.Call(
|
|
func=ast.Attribute(
|
|
value=ast.Name(id="self", ctx=ast.Load()),
|
|
attr=quantizer_names[0],
|
|
ctx=ast.Load(),
|
|
),
|
|
args=[node.left],
|
|
keywords=[],
|
|
)
|
|
if quantizer_names[1] is not None:
|
|
arg = node.right
|
|
if transpose:
|
|
arg = ast.Call(
|
|
func=ast.Attribute(
|
|
ast.Name(id="torch", ctx=ast.Load()),
|
|
attr="transpose",
|
|
ctx=ast.Load(),
|
|
),
|
|
args=[arg, ast.Constant(value=-1), ast.Constant(value=-2)],
|
|
keywords=[],
|
|
)
|
|
quant_arg = ast.Call(
|
|
func=ast.Attribute(
|
|
value=ast.Name(id="self", ctx=ast.Load()),
|
|
attr=quantizer_names[1],
|
|
ctx=ast.Load(),
|
|
),
|
|
args=[arg],
|
|
keywords=[],
|
|
)
|
|
if transpose:
|
|
quant_arg = ast.Call(
|
|
func=ast.Attribute(
|
|
ast.Name(id="torch", ctx=ast.Load()),
|
|
attr="transpose",
|
|
ctx=ast.Load(),
|
|
),
|
|
args=[quant_arg, ast.Constant(value=-1), ast.Constant(value=-2)],
|
|
keywords=[],
|
|
)
|
|
node.right = quant_arg
|
|
|
|
nodes = list(ast.walk(head))
|
|
org_class_name = nodes[1].name # type: ignore[attr-defined]
|
|
new_class_name = nodes[1].name = "_Quant" + nodes[1].name # type: ignore[attr-defined]
|
|
|
|
bmm_nodes = []
|
|
sdpa_nodes = []
|
|
bin_matmul_nodes = []
|
|
for node in ast.walk(head):
|
|
if is_bmm(node):
|
|
bmm_nodes.append(node)
|
|
if is_sdpa(node):
|
|
sdpa_nodes.append(node)
|
|
if is_bin_matmul(node):
|
|
bin_matmul_nodes.append(node)
|
|
if len(bmm_nodes) != 2 and len(sdpa_nodes) != 1 and len(bin_matmul_nodes) != 2:
|
|
print(f"Expect 2 bmm/matmul op in the {org_class_name}, found {len(bmm_nodes)}")
|
|
print(f"Or expect 1 sdpa op in the {org_class_name}, found {len(sdpa_nodes)}")
|
|
print(f"Or expect 2 @ op in the {org_class_name}, found {len(bin_matmul_nodes)}")
|
|
print("Auto quantization of KV Cache fails")
|
|
return False
|
|
|
|
if len(bmm_nodes) == 2:
|
|
# transpose k cache here to enable per-token quantization
|
|
# without transpose, the quantization will be per-channel, i.e.,
|
|
# self.k_bmm_quantizer(key_states.transpose(-1, -2))
|
|
# after transpose, the quantization will be per-token, i.e.,
|
|
# self.k_bmm_quantizer(key_states.transpose(-1, -2).transpose(-1, -2)).transpose(-1, -2)
|
|
# removing the additional tranpose is doable but not trivial
|
|
patch(bmm_nodes[0], quantizer_names=(None, "v_bmm_quantizer"))
|
|
patch(bmm_nodes[1], quantizer_names=("q_bmm_quantizer", "k_bmm_quantizer"), transpose=True)
|
|
print("Patching 2 BMM/Matmul operators with quantizers")
|
|
if len(bin_matmul_nodes) == 2:
|
|
patch_binop(
|
|
bin_matmul_nodes[1],
|
|
quantizer_names=("q_bmm_quantizer", "k_bmm_quantizer"),
|
|
transpose=True,
|
|
)
|
|
patch_binop(bin_matmul_nodes[0], quantizer_names=(None, "v_bmm_quantizer"))
|
|
print("Patching 2 @ operators with quantizers")
|
|
|
|
if len(sdpa_nodes) == 1:
|
|
patch(
|
|
sdpa_nodes[0], quantizer_names=("q_bmm_quantizer", "k_bmm_quantizer", "v_bmm_quantizer")
|
|
)
|
|
print("Patching 1 scaled_dot_product_attention operator with quantizers")
|
|
|
|
head = ast.fix_missing_locations(head)
|
|
org_class = model_module.__dict__[org_class_name]
|
|
|
|
quant_class = _create_quantized_class_from_ast(head, org_class, new_class_name, model_module)
|
|
register(original_cls=org_class, quantized_cls=quant_class)
|
|
print(f"Successfully registered {org_class_name} for quantization")
|
|
return True
|
|
|
|
|
|
def _create_quantized_class_from_ast(
|
|
head, org_class, new_class_name, model_module, temp_file_name=None
|
|
):
|
|
"""Create a quantized class from an AST representation.
|
|
|
|
Args:
|
|
head: The AST head containing the modified class definition
|
|
org_class: The original class to be quantized
|
|
new_class_name: Name for the new quantized class
|
|
model_module: The module containing the original class
|
|
temp_file_name: Optional file name to save the generated code
|
|
|
|
Returns:
|
|
The newly created quantized class
|
|
"""
|
|
head = ast.fix_missing_locations(head)
|
|
|
|
# Save the generated code to a temporary file if requested
|
|
module_code_str = ast.unparse(head)
|
|
if temp_file_name is None:
|
|
with tempfile.NamedTemporaryFile(
|
|
prefix="modelopt_", suffix=".py", delete=False
|
|
) as temp_file:
|
|
temp_file.write(module_code_str.encode())
|
|
temp_file_name = temp_file.name
|
|
print(f"Definition of {new_class_name} saved to {temp_file_name}")
|
|
else:
|
|
with open(temp_file_name, "w") as f:
|
|
f.write(module_code_str)
|
|
|
|
# Exec with python runtime and extract the new class
|
|
# This could lead to side effects if the class code is not properly isolated
|
|
# Therefore, it is recommended to run this function only when necessary
|
|
# exec(
|
|
# new_class_code,
|
|
# globals=model_module.__dict__,
|
|
# locals=model_module.__dict__
|
|
# ) # bandit throws error here
|
|
# quant_class = model_module.__dict__[new_class_name]
|
|
|
|
# Extract the bytecode and create a new class on the fly
|
|
# This is more tricky but doesn't require runtime execution
|
|
module_code = compile(head, filename=f"{temp_file_name}", mode="exec")
|
|
class_code = module_code.co_consts[0]
|
|
assert class_code.co_name == new_class_name
|
|
method_codes = [const for const in class_code.co_consts if isinstance(const, types.CodeType)]
|
|
|
|
new_methods = {}
|
|
for method_code in method_codes:
|
|
method_name = method_code.co_name
|
|
original_method = getattr(org_class, method_name, None)
|
|
if not isinstance(original_method, types.FunctionType):
|
|
continue
|
|
|
|
# Check if the method is a decorated method
|
|
# The exec path can handle decorated methods, but the safety compliance disallows exec
|
|
closure = original_method.__closure__
|
|
globals = original_method.__globals__
|
|
if method_code.co_freevars != original_method.__code__.co_freevars:
|
|
warn(f"{new_class_name}.{method_name} is a decorated method. Ignoring the decorator!")
|
|
|
|
new_closure = ()
|
|
for freevar in method_code.co_freevars:
|
|
assert freevar in original_method.__closure__
|
|
new_closure += (
|
|
original_method.__closure__[ # type: ignore[index]
|
|
original_method.__code__.co_freevars.index(freevar)
|
|
],
|
|
)
|
|
closure = new_closure
|
|
for closure_item in original_method.__closure__: # type: ignore[union-attr]
|
|
item = closure_item.cell_contents
|
|
if isinstance(item, types.FunctionType) and item.__name__ == method_name:
|
|
globals = item.__globals__
|
|
break
|
|
else:
|
|
raise ValueError(f"Cannot find the original method {method_name} in the closure")
|
|
|
|
# Create a new class method from bytecode
|
|
new_method = types.FunctionType(method_code, globals=globals, closure=closure)
|
|
new_method.__annotations__ = original_method.__annotations__
|
|
new_method.__defaults__ = original_method.__defaults__
|
|
new_method.__kwdefaults__ = original_method.__kwdefaults__
|
|
new_methods[method_name] = new_method
|
|
|
|
def setup_method(self):
|
|
self.q_bmm_quantizer = TensorQuantizer()
|
|
self.k_bmm_quantizer = TensorQuantizer()
|
|
self.v_bmm_quantizer = TensorQuantizer()
|
|
|
|
assert "_setup" not in new_methods, "Method _setup already exists"
|
|
new_methods["_setup"] = setup_method
|
|
|
|
# Create a new subclass on the fly
|
|
quant_class = type(new_class_name, (org_class,), new_methods)
|
|
|
|
return quant_class
|