Files
wehub-resource-sync 26446540fa
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

246 lines
8.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=invalid-name, missing-function-docstring, not-callable
# pylint: disable=import-outside-toplevel, unused-argument, use-list-literal
# mypy: ignore-errors
"""PyTorch Dynamo backend of Relax."""
import functools
import tvm_ffi
import tvm
from tvm.relax import build as relax_build
from .fx_translator import from_fx
def device_from_inputs(example_inputs):
for x in example_inputs:
if hasattr(x, "device"):
return x.device
return None
def relax_dynamo(pipeline: tvm.transform.Pass | None = None):
"""A helper function to create a relax backend.
Parameters
----------
pipeline : Optional[tvm.transform.Pass]
The pipeline to be applied to the relax module before sent to build.
Returns
-------
backend : Callable[[torch.fx.GraphModule, List[torch.Tensor]], Callable]
The relax dynamo backend.
"""
def _relax_backend(graph_module, example_inputs):
import torch # type: ignore[import]
assert isinstance(graph_module, torch.fx.GraphModule)
def to_torch_tensor(nd_tensor):
"""A helper function to transfer a Tensor to torch.tensor."""
if isinstance(nd_tensor, torch.Tensor):
# tvm-ffi #517 (Recursive DLPack container conversion) auto-converts
# ffi::Tensor items returned in containers back to torch.Tensor when
# the call site passed torch.Tensor inputs.
return nd_tensor
if isinstance(nd_tensor, tvm.runtime.Tensor):
return torch.from_numpy(nd_tensor.numpy())
elif isinstance(nd_tensor, tvm_ffi.Array):
return tuple(to_torch_tensor(x) for x in nd_tensor)
else:
raise ValueError(f"Unsupported type {type(nd_tensor)}")
graph_module.graph.eliminate_dead_code()
device = device_from_inputs(example_inputs)
assert len(example_inputs)
fake_inputs = []
if isinstance(example_inputs[0], torch._subclasses.fake_tensor.FakeTensor):
# Fake tensors
fake_inputs = example_inputs
else:
# Real tensors
for node in graph_module.graph.nodes:
if node.op != "placeholder":
continue
if "grapharg" not in node.meta:
continue
fake_tensor = node.meta["grapharg"].fake_tensor
if fake_tensor is None:
continue
fake_inputs.append(fake_tensor)
input_info = []
shape_vars = {}
for tensor in fake_inputs:
shape = []
for s in tensor.shape:
if isinstance(s, torch.SymInt):
if str(s) not in shape_vars:
shape_vars[str(s)] = tvm.tirx.Var(str(s), "int64")
shape.append(shape_vars[str(s)])
else:
shape.append(s)
input_info.append((shape, tensor.dtype))
mod = from_fx(graph_module, input_info)
if device.type == "cuda":
dev = tvm.cuda(device.index)
target = tvm.target.Target("cuda")
else:
dev = tvm.cpu(0)
target = tvm.target.Target(llvm_target())
# invoke optimization pipeline.
if pipeline is None:
# get default pipeline
seq = tvm.relax.get_pipeline()
elif isinstance(pipeline, str):
# lookup by name
seq = tvm.relax.get_pipeline(pipeline)
else:
seq = pipeline
mod = mod.with_attr("target", target)
mod = seq(mod)
ex = relax_build(mod, target=target)
vm = tvm.relax.VirtualMachine(ex.mod, device=dev)
def exec_tvm(*i_args):
args = [a.contiguous() for a in i_args if isinstance(a, torch.Tensor)]
vm_args = list()
for arg in args:
if arg.requires_grad:
arg = arg.detach()
if isinstance(arg, torch._subclasses.fake_tensor.FakeTensor):
# Materialize a real (eager) Tensor
arg = torch.randn(arg.shape, dtype=arg.dtype, device=device)
vm_args.append(arg)
outputs = vm["main"](*vm_args)
return to_torch_tensor(outputs)
return exec_tvm
return _relax_backend
def dynamo_capture_subgraphs(model, *params, **kwargs) -> tvm.IRModule:
"""Capture subgraphs of the PyTorch model using torch.compile into an IRModule.
Parameters
----------
model : torch.nn.Module
The PyTorch model to be captured.
params : List[torch.Tensor]
The parameters of the PyTorch model.
keep_params_as_input : bool
Whether to keep model parameters as input variables of the captured Relax functions.
Returns
-------
output : ImporterOutput
The output of translation, including the translated IRModule.
If `keep_params_as_input` is true, the functions in the IRModule have an
attribute "params" that contains the weights of the input model. The
weights can be detached by `relax.frontend.detach_params`.
"""
import torch # type: ignore[import]
from torch import _dynamo as dynamo # type: ignore[import]
from torch import fx # type: ignore[import]
keep_params_as_input = "keep_params_as_input" in kwargs and kwargs["keep_params_as_input"]
kwargs.pop("keep_params_as_input", None)
mod = tvm.IRModule()
def _capture(graph_module: fx.GraphModule, example_inputs):
assert isinstance(graph_module, torch.fx.GraphModule)
input_info = [(tuple(tensor.shape), str(tensor.dtype)) for tensor in example_inputs]
mod_ = from_fx(
graph_module,
input_info,
keep_params_as_input=keep_params_as_input,
unwrap_unit_return_tuple=True,
)
new_name = f"subgraph_{len(mod.get_global_vars())}"
mod[new_name] = mod_["main"].with_attr("global_symbol", new_name)
return graph_module.forward
dynamo.reset()
compiled_model = torch.compile(model, backend=_capture)
with torch.no_grad():
compiled_model(*params, **kwargs)
return mod
@functools.lru_cache(None)
def llvm_target():
import platform
import subprocess
AVX512_TARGET = {"kind": "llvm", "mcpu": "skylake-avx512"}
AVX2_TARGET = {"kind": "llvm", "mcpu": "core-avx2"}
DEFAULT_TARGET = "llvm"
system = platform.system()
if system == "Linux":
try:
with open("/proc/cpuinfo") as f:
cpuinfo = f.read()
if "avx512" in cpuinfo:
return AVX512_TARGET
return AVX2_TARGET
except FileNotFoundError:
pass
elif system == "Darwin":
try:
result = subprocess.run(
["sysctl", "-n", "machdep.cpu.features"],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0:
cpu_features = result.stdout.lower()
if "avx512" in cpu_features:
return AVX512_TARGET
if "avx2" in cpu_features:
return AVX2_TARGET
except (FileNotFoundError, subprocess.SubprocessError):
pass
if platform.machine() == "arm64":
return DEFAULT_TARGET
# Default fallback
return DEFAULT_TARGET