This is an automated email from the ASF dual-hosted git repository.
tlopex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/main by this push:
new 2c76c7955a [Relax][Frontend] Add TFLite Frontend Support for
CONV_3D_TRANSPOSE (#19530)
2c76c7955a is described below
commit 2c76c7955a6890d66306799e9877533fc0541aaf
Author: Wei-Cheng Hsu <[email protected]>
AuthorDate: Mon May 11 20:53:32 2026 +0800
[Relax][Frontend] Add TFLite Frontend Support for CONV_3D_TRANSPOSE (#19530)
This commit adds support for the CONV_3D_TRANSPOSE operator in the Relax
TFLite frontend.
Key implementations:
- Registered CONV_3D_TRANSPOSE to the TFLite op map.
- Implemented convert_conv3d_transpose which shares Conv3DOptions with
regular Conv3D but handles the distinct tensor input layout
[output_shape, weight, data, bias] and the DHWOI kernel layout.
- Added calculation for SAME padding that correctly handles transposed
convolution semantics, computing padding and output_padding based on
dilated kernel and stride sizes.
- Added comprehensive unit tests for valid and same padding in
test_frontend_tflite.py.
Testing:
- `python3 -m pytest tests/python/relax/test_frontend_tflite.py -k
"test_conv3d_transpose"`
Related to: https://github.com/apache/tvm/issues/19519
---
.../tvm/relax/frontend/tflite/tflite_frontend.py | 148 +++++++++++++++++++++
tests/python/relax/test_frontend_tflite.py | 103 ++++++++++++++
2 files changed, 251 insertions(+)
diff --git a/python/tvm/relax/frontend/tflite/tflite_frontend.py
b/python/tvm/relax/frontend/tflite/tflite_frontend.py
index d70f5d837e..376f14138b 100644
--- a/python/tvm/relax/frontend/tflite/tflite_frontend.py
+++ b/python/tvm/relax/frontend/tflite/tflite_frontend.py
@@ -133,6 +133,7 @@ class OperatorConverter:
"CONCATENATION": self.convert_concatenation,
"CONV_2D": functools.partial(self.convert_conv,
conv_type="conv2d"),
"CONV_3D": self.convert_conv3d,
+ "CONV_3D_TRANSPOSE": self.convert_conv3d_transpose,
"COS": functools.partial(self._convert_unary_elemwise,
relax_op=_op.cos),
"CUMSUM": self.convert_cumsum,
"DENSIFY": self.convert_densify,
@@ -2586,6 +2587,153 @@ class OperatorConverter:
out = self.convert_fused_activation_function(out, fused_activation_fn)
return out
+ def convert_conv3d_transpose(self, op):
+ """3D transposed convolution implementation."""
+
+ from tflite.BuiltinOptions import BuiltinOptions
+ from tflite.Conv3DOptions import Conv3DOptions
+ from tflite.Padding import Padding
+ from tflite.TensorType import TensorType
+
+ input_tensors = self.get_input_tensors(op)
+ assert len(input_tensors) >= 3, "input tensors length should be >= 3"
+
+ # TFLite CONV_3D_TRANSPOSE input order:
+ # [0] output_shape, [1] weight, [2] data, [3] bias (optional)
+ weight_tensor = input_tensors[1]
+ input_tensor = input_tensors[2]
+ input_tensor_idx = input_tensor.tensor_idx
+
+ output_tensors = self.get_output_tensors(op)
+ assert len(output_tensors) == 1, "output tensors length should be 1"
+ output_tensor = output_tensors[0]
+
+ assert op.BuiltinOptionsType() == BuiltinOptions.Conv3DOptions
+ op_options = op.BuiltinOptions()
+ conv3d_options = Conv3DOptions()
+ conv3d_options.Init(op_options.Bytes, op_options.Pos)
+
+ stride_d = conv3d_options.StrideD()
+ stride_h = conv3d_options.StrideH()
+ stride_w = conv3d_options.StrideW()
+ dilation_d = conv3d_options.DilationDFactor()
+ dilation_h = conv3d_options.DilationHFactor()
+ dilation_w = conv3d_options.DilationWFactor()
+ padding = conv3d_options.Padding()
+ fused_activation_fn = conv3d_options.FusedActivationFunction()
+
+ _, input_d, input_h, input_w, input_c =
to_int_list(self.get_tensor_shape(input_tensor))
+
+ # TFLite Conv3DTranspose kernel layout is DHWOI:
+ # KD KH KW OC IC
+ kernel_d, kernel_h, kernel_w, output_channels, in_channels =
to_int_list(
+ self.get_tensor_shape(weight_tensor)
+ )
+
+ dilated_kernel_d = dilation_d * (kernel_d - 1) + 1
+ dilated_kernel_h = dilation_h * (kernel_h - 1) + 1
+ dilated_kernel_w = dilation_w * (kernel_w - 1) + 1
+
+ params = {
+ "strides": [stride_d, stride_h, stride_w],
+ "dilation": [dilation_d, dilation_h, dilation_w],
+ "padding": [0, 0, 0, 0, 0, 0],
+ "output_padding": [0, 0, 0],
+ "data_layout": "NDHWC",
+ "kernel_layout": "DHWOI",
+ }
+
+ if input_c != in_channels:
+ assert input_c % in_channels == 0, (
+ "Input channels is not divisible by kernel in_channels."
+ )
+ params["groups"] = int(input_c / in_channels)
+
+ # weight tensor type should be INT8/UINT8 (quantization) or FLOAT32
+ weight_tensor_type = weight_tensor.tensor.Type()
+ assert weight_tensor_type in (
+ TensorType.INT8,
+ TensorType.UINT8,
+ TensorType.FLOAT32,
+ )
+ weight_tensor_type_str = self.get_tensor_type_str(weight_tensor_type)
+
+ in_expr = self.get_expr(input_tensor_idx)
+
+ # TFLite Conv3DTranspose kernel is already in DHWOI layout, no
transpose needed.
+ if self.has_expr(weight_tensor.tensor_idx):
+ weight_expr = self.get_expr(weight_tensor.tensor_idx)
+ else:
+ if self.is_prefetched(weight_tensor.tensor_idx):
+ weight_value =
self.get_prefetched_node(weight_tensor.tensor_idx)
+ else:
+ weight_value = self.get_tensor_value(weight_tensor)
+
+ weight_expr = self.exp_tab.new_const(
+ weight_value, dtype=weight_tensor_type_str,
+ source_name=weight_tensor.tensor.Name()
+ )
+
+ if padding == Padding.VALID:
+ pass
+ elif padding == Padding.SAME:
+ # For transposed convolution with SAME padding:
+ # target output_size = input_size * stride
+ # total_pad = max(0, dilated_kernel - stride)
+ for dim_kernel, dim_stride, label in [
+ (dilated_kernel_d, stride_d, "D"),
+ (dilated_kernel_h, stride_h, "H"),
+ (dilated_kernel_w, stride_w, "W"),
+ ]:
+ total_pad = max(0, dim_kernel - dim_stride)
+ pad_before = total_pad // 2
+ pad_after = total_pad - pad_before
+ idx = {"D": 0, "H": 1, "W": 2}[label]
+ params["padding"][idx] = pad_before
+ params["padding"][idx + 3] = pad_after
+
+ # output_padding handles the case when stride > dilated_kernel
+ output_pad = max(0, dim_stride - dim_kernel)
+ params["output_padding"][idx] = output_pad
+ else:
+ raise tvm.error.OpAttributeUnImplemented(
+ f"Padding format {padding} is not supported for operator
Conv3DTranspose."
+ )
+
+ if input_tensor.qnn_params:
+ raise tvm.error.OpNotImplemented(
+ "Quantized Conv3DTranspose is not yet supported in the Relax
frontend."
+ )
+
+ out = relax.op.nn.conv3d_transpose(in_expr, weight_expr, **params)
+
+ # if we have bias (input_tensors[3])
+ if len(input_tensors) >= 4:
+ bias_tensor = input_tensors[3]
+ if bias_tensor.tensor_idx != -1:
+ bias_tensor_type = bias_tensor.tensor.Type()
+ # bias tensor type should be INT32 (int8 qnn) or INT64 (int16
qnn) or FLOAT32
+ assert bias_tensor_type in (TensorType.INT32,
TensorType.INT64, TensorType.FLOAT32)
+ bias_tensor_type_str =
self.get_tensor_type_str(bias_tensor_type)
+ if self.has_expr(bias_tensor.tensor_idx):
+ bias_expr = self.get_expr(bias_tensor.tensor_idx)
+ else:
+ bias_expr = self.exp_tab.new_const(
+ self.get_tensor_value(bias_tensor),
+ dtype=bias_tensor_type_str,
+ source_name=bias_tensor.tensor.Name(),
+ )
+ out = relax.op.add(out, bias_expr)
+
+ # Handle fused activation.
+ if output_tensor.qnn_params:
+ raise tvm.error.OpNotImplemented(
+ "Quantized Conv3DTranspose is not yet supported in the Relax
frontend."
+ )
+
+ out = self.convert_fused_activation_function(out, fused_activation_fn)
+ return out
+
def convert_split(self, op):
"""split implementation."""
diff --git a/tests/python/relax/test_frontend_tflite.py
b/tests/python/relax/test_frontend_tflite.py
index d0401e4649..9b9029b5a5 100644
--- a/tests/python/relax/test_frontend_tflite.py
+++ b/tests/python/relax/test_frontend_tflite.py
@@ -1694,6 +1694,109 @@ def test_conv3d_same():
verify(Conv3DModule, Expected)
+def _make_conv3d_transpose_module(data_shape, kernel_shape, strides, padding):
+ # Compute the expected output_shape for tf.nn.conv3d_transpose.
+ # data_shape: (N, D, H, W, C_in), kernel_shape: (KD, KH, KW, C_out, C_in)
+ # strides: (1, sD, sH, sW, 1)
+ batch = data_shape[0]
+ out_channels = kernel_shape[3]
+ out_spatial = []
+ for i in range(3): # D, H, W
+ in_size = data_shape[1 + i]
+ k_size = kernel_shape[i]
+ s = strides[1 + i]
+ if padding == "VALID":
+ out_spatial.append((in_size - 1) * s + k_size)
+ else: # SAME
+ out_spatial.append(in_size * s)
+ computed_output_shape = [batch] + out_spatial + [out_channels]
+
+ class Conv3DTransposeModule(tf.Module):
+ @tf.function(
+ input_signature=[
+ tf.TensorSpec(shape=data_shape, dtype=tf.float32),
+ tf.TensorSpec(shape=kernel_shape, dtype=tf.float32),
+ ]
+ )
+ def func(self, data, kernel):
+ return tf.nn.conv3d_transpose(
+ input=data,
+ filters=kernel,
+ output_shape=computed_output_shape,
+ strides=strides,
+ padding=padding,
+ )
+
+ return Conv3DTransposeModule
+
+
+
+def test_conv3d_transpose_valid():
+ Conv3DTransposeModule = _make_conv3d_transpose_module(
+ (1, 8, 8, 8, 3), (3, 3, 3, 8, 3), (1, 1, 1, 1, 1), "VALID"
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(
+ data: R.Tensor((1, 8, 8, 8, 3), dtype="float32"),
+ kernel: R.Tensor((3, 3, 3, 8, 3), dtype="float32"),
+ ) -> R.Tensor((1, 10, 10, 10, 8), dtype="float32"):
+ R.func_attr({"num_input": 2})
+ with R.dataflow():
+ gv: R.Tensor((1, 10, 10, 10, 8), dtype="float32") =
R.nn.conv3d_transpose(
+ data,
+ kernel,
+ strides=[1, 1, 1],
+ padding=[0, 0, 0, 0, 0, 0],
+ output_padding=[0, 0, 0],
+ dilation=[1, 1, 1],
+ groups=1,
+ data_layout="NDHWC",
+ kernel_layout="DHWOI",
+ out_layout="NDHWC",
+ out_dtype="void",
+ )
+ R.output(gv)
+ return gv
+
+ verify(Conv3DTransposeModule, Expected)
+
+
+def test_conv3d_transpose_same():
+ Conv3DTransposeModule = _make_conv3d_transpose_module(
+ (1, 8, 8, 8, 3), (3, 3, 3, 8, 3), (1, 1, 1, 1, 1), "SAME"
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(
+ data: R.Tensor((1, 8, 8, 8, 3), dtype="float32"),
+ kernel: R.Tensor((3, 3, 3, 8, 3), dtype="float32"),
+ ) -> R.Tensor((1, 8, 8, 8, 8), dtype="float32"):
+ R.func_attr({"num_input": 2})
+ with R.dataflow():
+ gv: R.Tensor((1, 8, 8, 8, 8), dtype="float32") =
R.nn.conv3d_transpose(
+ data,
+ kernel,
+ strides=[1, 1, 1],
+ padding=[1, 1, 1, 1, 1, 1],
+ output_padding=[0, 0, 0],
+ dilation=[1, 1, 1],
+ groups=1,
+ data_layout="NDHWC",
+ kernel_layout="DHWOI",
+ out_layout="NDHWC",
+ out_dtype="void",
+ )
+ R.output(gv)
+ return gv
+
+ verify(Conv3DTransposeModule, Expected)
+
+
def _make_pool2d_module(pool, data_shape, ksize, data_format, strides,
padding):
class Pool2DModule(tf.Module):
@tf.function(