Constant-fold FLOOR_DIV.

tfl.floor_div had no folder, so a FLOOR_DIV on constant operands survived
conversion as a runtime op. GPU delegates that reject FLOOR_DIV then see a
CPU-only op in the middle of an otherwise delegable graph.

The fold matches the TFLite kernel: f32 operands are divided and floored in
double, and integer division rounds toward negative infinity. Division by
zero (for any element type) is left to the runtime, which reports it as an
error.

HF DETR models hit this in the sine position embedding (dim_t // 2 on a
constant arange); it is their only FLOOR_DIV.

Tests: const-fold.mlir adds f32 (with broadcast, and a 1.0 // 0.1 case that
needs the double division) and i32 (all sign combinations) folds, plus
divide-by-zero (i32 and f32) and runtime-operand negatives.
PiperOrigin-RevId: 1004127675
This commit is contained in:
A. Unique TensorFlower authored and TensorFlower Gardener committed 2026-10-21 22:04:46 -07:00
1 parent fd30f24418
commit 72e90de7db
3 files changed
+126

No files matched your search

@@ -3510,6 +3510,55 @@ int64_t DivOp::GetArithmeticCount(Operation* op) {
return -1;
}
//===----------------------------------------------------------------------===//
// FloorDivOp
//===----------------------------------------------------------------------===//
OpFoldResult FloorDivOp::fold(FoldAdaptor adaptor) {
if (!ShouldFoldOperation(this->getOperation())) return {};
if (llvm::isa<quant::QuantizedType>(getType().getElementType())) {
return {};
}
auto lhs = llvm::dyn_cast_or_null<ElementsAttr>(adaptor.getLhs());
auto rhs = llvm::dyn_cast_or_null<ElementsAttr>(adaptor.getRhs());
if (!lhs || !rhs) return {};
// The TFLite kernel fails on a zero divisor for every element type; leave
// such ops to the runtime instead of folding them to a value (or inf/NaN).
const Type element_type = getType().getElementType();
if (element_type.isSignlessInteger()) {
auto int_rhs = llvm::dyn_cast<DenseIntElementsAttr>(rhs);
if (!int_rhs) return {};
for (const APInt& v : int_rhs.getValues<APInt>()) {
if (v.isZero()) return {};
}
} else if (element_type.isF32()) {
auto float_rhs = llvm::dyn_cast<DenseFPElementsAttr>(rhs);
if (!float_rhs) return {};
for (const APFloat& v : float_rhs.getValues<APFloat>()) {
if (v.isZero()) return {};
}
} else {
return {};
}
// Matches the TFLite kernel: floats are divided and floored in double (see
// reference_ops::FloorDiv), and integer division rounds toward negative
// infinity.
return ConstFoldBinaryOp(
getType(), getOperands(),
[](APFloat a, APFloat b) {
const double q = std::floor(static_cast<double>(a.convertToFloat()) /
static_cast<double>(b.convertToFloat()));
return APFloat(static_cast<float>(q));
},
[](APInt a, APInt b) {
return llvm::APIntOps::RoundingSDiv(a, b, APInt::Rounding::DOWN);
});
}
//===----------------------------------------------------------------------===//
// PackOp
//===----------------------------------------------------------------------===//
@@ -1996,6 +1996,8 @@ def TFL_FloorDivOp : TFL_Op<"floor_div", [
let builders = [TFL_BroadcastableBinaryBuilder];
let hasFolder = 1;
let hasCustomAssemblyFormat = 1;
let extraClassDefinition = [{
@@ -67,6 +67,81 @@ func.func @cumsum_chained_pipeline() -> tensor<3xf32> {
func.return %res_pipeline : tensor<3xf32>
}
// The DETR sine position embedding pattern: dim_t // 2 on a constant arange.
// CHECK-LABEL: @floor_div_f32
func.func @floor_div_f32() -> tensor<4xf32> {
%lhs = arith.constant dense<[0.0, 3.0, -3.0, 7.5]> : tensor<4xf32>
%rhs = arith.constant dense<2.0> : tensor<f32>
// CHECK: %[[CST:.*]] = arith.constant dense<[0.000000e+00, 1.000000e+00, -2.000000e+00, 3.000000e+00]> : tensor<4xf32>
// CHECK-NOT: tfl.floor_div
// CHECK: return %[[CST]]
%0 = "tfl.floor_div"(%lhs, %rhs) : (tensor<4xf32>, tensor<f32>) -> tensor<4xf32>
func.return %0 : tensor<4xf32>
}
// Rounds toward negative infinity, not toward zero.
// CHECK-LABEL: @floor_div_i32
func.func @floor_div_i32() -> tensor<4xi32> {
%lhs = arith.constant dense<[7, -7, 7, -7]> : tensor<4xi32>
%rhs = arith.constant dense<[2, 2, -2, -2]> : tensor<4xi32>
// CHECK: %[[CST:.*]] = arith.constant dense<[3, -4, -4, 3]> : tensor<4xi32>
// CHECK-NOT: tfl.floor_div
// CHECK: return %[[CST]]
%0 = "tfl.floor_div"(%lhs, %rhs) : (tensor<4xi32>, tensor<4xi32>) -> tensor<4xi32>
func.return %0 : tensor<4xi32>
}
// CHECK-LABEL: @floor_div_i32_by_zero_not_folded
func.func @floor_div_i32_by_zero_not_folded() -> tensor<2xi32> {
%lhs = arith.constant dense<[4, 5]> : tensor<2xi32>
%rhs = arith.constant dense<[2, 0]> : tensor<2xi32>
// CHECK: tfl.floor_div
%0 = "tfl.floor_div"(%lhs, %rhs) : (tensor<2xi32>, tensor<2xi32>) -> tensor<2xi32>
func.return %0 : tensor<2xi32>
}
// Like the kernel, divides in double: 1.0f / 0.1f rounds to exactly 10.0f in
// f32, but is 9.99999985 in double, so the floor is 9.
// CHECK-LABEL: @floor_div_f32_divides_in_double
func.func @floor_div_f32_divides_in_double() -> tensor<f32> {
%lhs = arith.constant dense<1.0> : tensor<f32>
%rhs = arith.constant dense<0.1> : tensor<f32>
// CHECK: %[[CST:.*]] = arith.constant dense<9.000000e+00> : tensor<f32>
// CHECK-NOT: tfl.floor_div
// CHECK: return %[[CST]]
%0 = "tfl.floor_div"(%lhs, %rhs) : (tensor<f32>, tensor<f32>) -> tensor<f32>
func.return %0 : tensor<f32>
}
// CHECK-LABEL: @floor_div_f32_by_zero_not_folded
func.func @floor_div_f32_by_zero_not_folded() -> tensor<2xf32> {
%lhs = arith.constant dense<[4.0, 5.0]> : tensor<2xf32>
%rhs = arith.constant dense<[2.0, -0.0]> : tensor<2xf32>
// CHECK: tfl.floor_div
%0 = "tfl.floor_div"(%lhs, %rhs) : (tensor<2xf32>, tensor<2xf32>) -> tensor<2xf32>
func.return %0 : tensor<2xf32>
}
// CHECK-LABEL: @floor_div_runtime_not_folded
func.func @floor_div_runtime_not_folded(%arg0: tensor<4xf32>) -> tensor<4xf32> {
%rhs = arith.constant dense<2.0> : tensor<f32>
// CHECK: tfl.floor_div
%0 = "tfl.floor_div"(%arg0, %rhs) : (tensor<4xf32>, tensor<f32>) -> tensor<4xf32>
func.return %0 : tensor<4xf32>
}
// CHECK-LABEL: @rank
func.func @rank() -> tensor<1xi32> {
%cst = arith.constant dense<[[1], [2]]> : tensor<2x1xi32>