Files
apache--tvm/python/tvm/relax/analysis/estimate_memory_usage.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

183 lines
7.2 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# pylint: disable=abstract-method,unused-argument
# pylint: disable=missing-function-docstring,missing-module-docstring
import tvm
from tvm.ir import Call, Op
from tvm.ir.module import IRModule
from ..expr import Expr, Function, ShapeExpr
from ..expr_functor import PyExprVisitor, visitor
def estimate_memory_usage(mod: IRModule | Function) -> str:
"""Analysis function that estimates the memory usage of Relax functions
in an IRModule. The estimation includes the total memory size needed to
be allocated before and after memory planning.
The result might be over-estimated, as the estimation is static, which
does not consider control flows (such as "if" and cross-function calls).
It simply accumulates the size of every alloc_tensor and alloc_storage.
This analysis function is used to demonstrate the effect of memory
planning.
Parameters
----------
mod : Union[IRModule, Function]
The input IRModule whose functions inside are to be analyzed.
If the input is a Function, we will wrap it with a IRModule, with
the function named "main".
Returns
-------
est : str
The estimation information, in the form of a string.
Notes
-----
We regards "relax.memory.alloc_tensor/storage" as the results produced by memory planning.
"""
@visitor
class MemoryEstimator(PyExprVisitor):
"""The IR visitor which estimates the memory usage of each Relax function.
Attributes
----------
total_alloc_tensor_mem : int
The total memory size of alloc_tensor, in bytes.
total_const_size_tensor_num : int
The number of constant-size tensors.
total_dyn_size_tensor_num : int
The number of dynamic-size tensors.
planned_alloc_mem : int
The total memory size of memory.alloc_storage after memory planning, in bytes.
planned_mem_num : int
The number of memory.alloc_storages.
"""
total_alloc_tensor_mem: int
total_const_size_tensor_num: int
total_dyn_size_tensor_num: int
planned_alloc_mem: int
planned_mem_num: int
builtin_alloc_tensor_op = Op.get("relax.builtin.alloc_tensor")
memory_alloc_tensor_op = Op.get("relax.memory.alloc_tensor")
memory_alloc_storage_op = Op.get("relax.memory.alloc_storage")
def estimate(self, mod: IRModule) -> str:
estimation: str = ""
for global_var, func in mod.functions_items():
if not isinstance(func, Function):
continue
self.cleanup()
self.visit_expr(func)
estimation += self.generate_est_string(global_var.name_hint) + "\n"
if estimation != "":
estimation = "Memory usage estimation:\n" + estimation
return estimation
def cleanup(self) -> None:
self.total_alloc_tensor_mem = 0
self.total_const_size_tensor_num = 0
self.total_dyn_size_tensor_num = 0
self.planned_alloc_mem = 0
self.planned_mem_num = 0
def visit_call_(self, call: Call) -> None: # pylint: disable=arguments-differ
if call.op == self.builtin_alloc_tensor_op:
self.accumulate_builtin_tensor_alloc(
shape=call.args[0], dtype_str=call.args[1].value
)
elif call.op == self.memory_alloc_tensor_op:
self.accumulate_tensor_alloc(shape=call.args[2], dtype_str=call.args[3].value)
elif call.op == self.memory_alloc_storage_op:
self.accumulate_storage_alloc(size=call.args[0])
def calculate_size(self, shape: Expr, dtype_str: str) -> int:
if not isinstance(shape, ShapeExpr):
raise TypeError(
"The shape of relax.builtin.alloc_tensor and "
"relax.memory.alloc_tensor is expected to be ShapeExpr"
)
size: int = 1
for dim_len in shape.values:
if not isinstance(dim_len, tvm.tirx.IntImm):
self.total_dyn_size_tensor_num += 1
return -1
size *= dim_len.value
dtype = tvm.DataType(dtype_str)
return size * ((dtype.bits + 7) // 8) * dtype.lanes
def accumulate_builtin_tensor_alloc(self, shape: Expr, dtype_str: str) -> None:
size = self.calculate_size(shape, dtype_str)
if size == -1:
return
self.total_const_size_tensor_num += 1
self.total_alloc_tensor_mem += size
self.planned_mem_num += 1
self.planned_alloc_mem += size
def accumulate_tensor_alloc(self, shape: Expr, dtype_str: str) -> None:
size = self.calculate_size(shape, dtype_str)
if size == -1:
return
self.total_const_size_tensor_num += 1
self.total_alloc_tensor_mem += size
def accumulate_storage_alloc(self, size: Expr) -> None:
if not isinstance(size, ShapeExpr):
raise TypeError(
"The size of relax.memory.alloc_storage is expected to be ShapeExpr"
)
self.planned_mem_num += 1
self.planned_alloc_mem += size.values[0].value
def generate_est_string(self, func_name: str) -> str:
est = (
f" * Without memory planning, there are {self.total_const_size_tensor_num} "
"constant-size memory allocation(s) with total size "
"{0:.4} GB".format(self.total_alloc_tensor_mem / 2**30)
)
if self.total_dyn_size_tensor_num > 0:
est += f", and {self.total_dyn_size_tensor_num} dynamic-size allocation(s)"
est += (
f".\n * With memory planning, there are {self.planned_mem_num} constant-size "
"memory allocation(s) with total size "
"{0:.4} GB.\n".format(self.planned_alloc_mem / 2**30)
)
if self.total_alloc_tensor_mem != 0:
est += (
" * Memory planning reduces constant memory size to "
f"{self.planned_alloc_mem / self.total_alloc_tensor_mem:.1%}."
)
return "- Function " + func_name + ":\n" + est
if isinstance(mod, Function):
mod = tvm.IRModule({tvm.ir.GlobalVar("foo"): mod})
return MemoryEstimator().estimate(mod)