Skip to content

noop_elimination silently drops the floor of floor_div(x, 1) for float inputs #2761

Description

@winklemad

Description

The noop_elimination pass removes floor_div(x, y=1) as an identity, but floor_div rounds toward negative infinity, so floor_div(x, 1) == floor(x) != x for non-integer float inputs. The pass therefore silently drops the floor and changes the model's numerics — no error, just wrong results. noop_elimination is in the common (default) pipeline.

Reproduction

from coremltools.converters.mil.mil import Builder as mb
from coremltools.converters.mil.mil.passes.pass_registry import PASS_REGISTRY

@mb.program(input_specs=[mb.TensorSpec(shape=(4,))])
def prog(x):
    r = mb.floor_div(x=x, y=1.0)
    return mb.add(x=r, y=1.0)

PASS_REGISTRY["common::noop_elimination"].apply(prog)
print([op.op_type for op in prog.functions["main"].operations])
# ['add']  -- floor_div silently removed; the model now computes x + 1 instead of floor(x) + 1

For x = [1.5, -0.5, 2.9, -2.1] the original graph computes floor(x) + 1 = [2, 0, 3, -1], but after the pass it computes x + 1 = [2.5, 0.5, 3.9, -1.1].

Cause

In noop_elimination.py, remove_elementwise groups floor_div with real_div and pow:

elif op.op_type in {"floor_div", "pow", "real_div"}:
    return _remove_elementwise_binary(op, None, 1)   # remove when divisor/exponent == 1

x / 1 == x and x ** 1 == x hold for all reals, but floor_div(x, 1) == floor(x) equals x only for integer x. floor_div is defined for fp16/fp32/i32 and there is no dtype guard, so the floor is dropped for float inputs.

Expected

floor_div(x, 1) should be removed only when x is integral, and kept for float inputs. PR incoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions