Skip to content

Commit b0d3c6b

Browse files
[Torch] Fix aten.linalg_vector_norm for ord = 0 and ord = +/-inf (#4718)
Fix `aten.linalg_vector_norm` for `ord = 0` and `ord = ±inf` The generic `(sum |x|^ord)^(1/ord)` lowering is wrong for these: `±inf` gives `inf^0 = 1` and `ord = 0` gives `pow(N, +inf)`, so `ord = -inf` was returning `1.0` instead of `min(|x_i|)`. Instead of patching each backend, I decompose the three cases in `DecomposeComplexOps` (following `_refs`): `+inf → amax(|x|)`, `-inf → amin(|x|)`, `0 → sum(|x| != 0)`. This also fixes the ONNX path (`LpNormalization → aten.norm.ScalarOpt_dim → aten.linalg_vector_norm`), which never hits the Python `_refs` decomposition. Finite / non-constant `ord` still go through the backends unchanged, which now decline `0`/`±inf` cleanly instead of miscompiling. Also drops the StableHLO XFAILs — the decomposition makes these correct there too.
1 parent 0029fc2 commit b0d3c6b

9 files changed

Lines changed: 582 additions & 37 deletions

File tree

lib/Conversion/TorchToLinalg/Reduction.cpp

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -678,16 +678,6 @@ class ConvertReductionOp : public ConversionPattern {
678678
// Cast `ord` to float so that we can readily pass it math.powf.
679679
Value ordValue = convertScalarToDtype(rewriter, loc, ordOp, elemType);
680680

681-
// TODO: Add support for ord = {0, +inf, -inf}.
682-
auto epsilon = 1e-5;
683-
auto ordLiteral = 0.0;
684-
if (matchPattern(ordValue, m_TorchConstantFloat(&ordLiteral)) &&
685-
fabs(ordLiteral) < epsilon)
686-
return rewriter.notifyMatchFailure(op, "unimplemented: L0 norm");
687-
688-
if (std::isinf(ordLiteral))
689-
return rewriter.notifyMatchFailure(op, "unimplemented: ord = +/- inf");
690-
691681
// Raise each summed value to the inverse of the order of the norm.
692682
TypedAttr oneAttr = rewriter.getFloatAttr(elemType, 1.0);
693683
auto oneValue = arith::ConstantOp::create(rewriter, loc, oneAttr);
@@ -759,6 +749,26 @@ class ConvertReductionOp : public ConversionPattern {
759749
return rewriter.notifyMatchFailure(
760750
op, "invalid operand or result types to use with linalg on tensors");
761751

752+
// ord = 0 (count of nonzeros, imported as an int) and ord = +/-inf (min/max
753+
// of absolute values, imported as a float) are handled by
754+
// DecomposeAtenLinalgVectorNormOp; the generic (sum |x|^ord)^(1/ord)
755+
// lowering is undefined for them. Decline before creating any IR so that a
756+
// miscompile cannot slip through if decomposition is disabled. Match on the
757+
// original `ord` scalar; a non-constant ord is left to the generic path.
758+
if (auto normOp = dyn_cast<AtenLinalgVectorNormOp>(op)) {
759+
double ordLiteral;
760+
int64_t ordInt;
761+
bool isConstOrd = true;
762+
if (matchPattern(normOp.getOrd(), m_TorchConstantInt(&ordInt)))
763+
ordLiteral = static_cast<double>(ordInt);
764+
else if (!matchPattern(normOp.getOrd(),
765+
m_TorchConstantFloat(&ordLiteral)))
766+
isConstOrd = false;
767+
if (isConstOrd && (ordLiteral == 0.0 || std::isinf(ordLiteral)))
768+
return rewriter.notifyMatchFailure(
769+
op, "ord = 0 / +/-inf are handled by decomposition");
770+
}
771+
762772
FailureOr<torch_to_linalg::ReductionOpInfo> opInfo =
763773
computeReductionOpInfo(op, operands, rewriter);
764774
if (failed(opInfo))

lib/Conversion/TorchToStablehlo/Reduction.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,23 @@ LogicalResult ConvertAtenReductionOp<AtenLinalgVectorNormOp>::matchAndRewrite(
910910
}
911911
int64_t inputRank = inputType.getRank();
912912

913+
// ord = 0 (count of nonzeros, imported as an int) and ord = +/-inf (min/max
914+
// of absolute values, imported as a float) are handled by
915+
// DecomposeAtenLinalgVectorNormOp; the generic (sum |x|^ord)^(1/ord) lowering
916+
// below is undefined for them. Decline before creating any IR so that a
917+
// miscompile cannot slip through if decomposition is disabled. Match on the
918+
// original `ord` scalar; a non-constant ord is left to the generic path.
919+
double ordFloat;
920+
int64_t ordInt;
921+
bool isConstOrd = true;
922+
if (matchPattern(op.getOrd(), m_TorchConstantInt(&ordInt)))
923+
ordFloat = static_cast<double>(ordInt);
924+
else if (!matchPattern(op.getOrd(), m_TorchConstantFloat(&ordFloat)))
925+
isConstOrd = false;
926+
if (isConstOrd && (ordFloat == 0.0 || std::isinf(ordFloat)))
927+
return rewriter.notifyMatchFailure(
928+
op, "ord = 0 / +/-inf are handled by decomposition");
929+
913930
auto outType =
914931
cast<RankedTensorType>(getTypeConverter()->convertType(op.getType()));
915932
auto outElemType = outType.getElementType();

lib/Conversion/TorchToTosa/TosaLegalizeCommon.cpp

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,44 +1121,46 @@ convertLinalgVectorNormOp(PatternRewriter &rewriter, Operation *op,
11211121
}
11221122

11231123
auto linalgVectorNormOp = cast<AtenLinalgVectorNormOp>(op);
1124-
// TODO: Add support for ord = {0, +inf, -inf}.
1125-
auto epsilon = 1e-5;
11261124
double ordLiteralFloat = 1.0;
11271125
int64_t ordLiteralInt = 1;
1128-
Value ordVal;
1129-
if (matchPattern(linalgVectorNormOp.getOrd(),
1130-
torch::Torch::m_TorchConstantFloat(&ordLiteralFloat))) {
1131-
ordVal = tosa::getConstTensor<float>(rewriter, op,
1132-
{static_cast<float>(ordLiteralFloat)},
1133-
{}, elemType)
1134-
.value();
1135-
} else if (matchPattern(linalgVectorNormOp.getOrd(),
1136-
torch::Torch::m_TorchConstantInt(&ordLiteralInt))) {
1137-
ordVal = tosa::getConstTensor<float>(rewriter, op,
1138-
{static_cast<float>(ordLiteralInt)},
1139-
{}, elemType)
1140-
.value();
1141-
} else {
1126+
bool ordIsFloat =
1127+
matchPattern(linalgVectorNormOp.getOrd(),
1128+
torch::Torch::m_TorchConstantFloat(&ordLiteralFloat));
1129+
bool ordIsInt =
1130+
!ordIsFloat &&
1131+
matchPattern(linalgVectorNormOp.getOrd(),
1132+
torch::Torch::m_TorchConstantInt(&ordLiteralInt));
1133+
if (!ordIsFloat && !ordIsInt) {
11421134
op->emitOpError("only support FP or INT type ord parameter");
11431135
return std::nullopt;
11441136
}
11451137

1146-
Value ordValRank0 = ordVal;
1147-
if (mlir::tosa::EqualizeRanks(rewriter, op->getLoc(), input_value, ordVal)
1148-
.failed())
1138+
// ord = 0 (count of nonzeros) and ord = +/-inf (min/max of absolute values)
1139+
// are handled by DecomposeAtenLinalgVectorNormOp; the generic
1140+
// (sum |x|^ord)^(1/ord) lowering below is undefined for them. Decline before
1141+
// creating any IR so that a miscompile cannot slip through if decomposition
1142+
// is disabled.
1143+
double ordLiteral =
1144+
ordIsFloat ? ordLiteralFloat : static_cast<double>(ordLiteralInt);
1145+
if (ordLiteral == 0.0) {
1146+
(void)rewriter.notifyMatchFailure(op,
1147+
"ord = 0 is handled by decomposition");
11491148
return std::nullopt;
1150-
1151-
if (fabs(ordLiteralFloat) < epsilon ||
1152-
fabs(static_cast<double>(ordLiteralInt)) < epsilon) {
1153-
op->emitOpError("unimplemented: L0 norm");
1149+
}
1150+
if (std::isinf(ordLiteral)) {
1151+
(void)rewriter.notifyMatchFailure(
1152+
op, "ord = +/-inf are handled by decomposition");
11541153
return std::nullopt;
11551154
}
11561155

1157-
if (std::isinf(ordLiteralFloat) ||
1158-
std::isinf(static_cast<double>(ordLiteralInt))) {
1159-
op->emitOpError("unimplemented: ord = +/- inf");
1156+
Value ordVal = tosa::getConstTensor<float>(rewriter, op,
1157+
{static_cast<float>(ordLiteral)},
1158+
{}, elemType)
1159+
.value();
1160+
Value ordValRank0 = ordVal;
1161+
if (mlir::tosa::EqualizeRanks(rewriter, op->getLoc(), input_value, ordVal)
1162+
.failed())
11601163
return std::nullopt;
1161-
}
11621164

11631165
auto input_value_casted =
11641166
tosa::tosaCastTensorToType(rewriter, input_value, output_type).value();

lib/Dialect/Torch/Transforms/DecomposeComplexOps.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10677,6 +10677,82 @@ class DecomposeAtenNormScalarOptDimOp
1067710677
};
1067810678
} // namespace
1067910679

10680+
namespace {
10681+
// Decompose `aten.linalg_vector_norm` for the `ord` values where the generic
10682+
// `(sum |x|^ord)^(1/ord)` lowering is undefined: `ord = +inf` is `max(|x|)`,
10683+
// `ord = -inf` is `min(|x|)`, and `ord = 0` is the count of nonzero elements
10684+
// `sum(|x| != 0)`. Handling these here makes every backend correct through the
10685+
// already-supported `aten.amax`/`aten.amin`/`aten.sum.dim_IntList` ops. Finite,
10686+
// nonzero `ord` (and non-constant `ord`) are left for the backends' generic
10687+
// lowering.
10688+
class DecomposeAtenLinalgVectorNormOp
10689+
: public OpRewritePattern<AtenLinalgVectorNormOp> {
10690+
public:
10691+
using OpRewritePattern::OpRewritePattern;
10692+
LogicalResult matchAndRewrite(AtenLinalgVectorNormOp op,
10693+
PatternRewriter &rewriter) const override {
10694+
Location loc = op.getLoc();
10695+
10696+
// `ord` is an `AnyTorchScalarType`, so an integer `ord` (e.g. `ord = 0`)
10697+
// imports as a `torch.constant.int`, not a `torch.constant.float`.
10698+
double ordLiteral;
10699+
int64_t ordInt;
10700+
if (matchPattern(op.getOrd(), m_TorchConstantInt(&ordInt)))
10701+
ordLiteral = static_cast<double>(ordInt);
10702+
else if (!matchPattern(op.getOrd(), m_TorchConstantFloat(&ordLiteral)))
10703+
return rewriter.notifyMatchFailure(op, "non-constant `ord` unsupported");
10704+
10705+
bool isInf = std::isinf(ordLiteral);
10706+
bool isZero = ordLiteral == 0.0;
10707+
if (!isInf && !isZero)
10708+
return rewriter.notifyMatchFailure(
10709+
op, "only ord = 0 / +/-inf are decomposed; finite ord is lowered by "
10710+
"the backends");
10711+
10712+
Value self = op.getSelf();
10713+
auto selfType = dyn_cast<BaseTensorType>(self.getType());
10714+
if (!selfType || !selfType.hasSizes())
10715+
return rewriter.notifyMatchFailure(op, "expected input with known rank");
10716+
10717+
// `dim = None` means reduce over all dimensions.
10718+
Value dim = op.getDim();
10719+
if (isa<Torch::NoneType>(dim.getType())) {
10720+
SmallVector<Value> allDims;
10721+
for (int64_t i = 0, rank = selfType.getSizes().size(); i < rank; ++i)
10722+
allDims.push_back(ConstantIntOp::create(rewriter, loc,
10723+
rewriter.getI64IntegerAttr(i)));
10724+
dim = PrimListConstructOp::create(
10725+
rewriter, loc,
10726+
Torch::ListType::get(Torch::IntType::get(op.getContext())), allDims);
10727+
}
10728+
10729+
Value abs = AtenAbsOp::create(rewriter, loc, self.getType(), self);
10730+
10731+
// `ord = +/-inf` are the max/min of the absolute values.
10732+
if (isInf) {
10733+
if (ordLiteral > 0)
10734+
rewriter.replaceOpWithNewOp<AtenAmaxOp>(op, op.getType(), abs, dim,
10735+
op.getKeepdim());
10736+
else
10737+
rewriter.replaceOpWithNewOp<AtenAminOp>(op, op.getType(), abs, dim,
10738+
op.getKeepdim());
10739+
return success();
10740+
}
10741+
10742+
// `ord = 0` is the count of nonzero elements: `sum(|x| != 0)`.
10743+
Value zero =
10744+
ConstantFloatOp::create(rewriter, loc, rewriter.getF64FloatAttr(0.0));
10745+
auto boolType = selfType.getWithSizesAndDtype(selfType.getOptionalSizes(),
10746+
rewriter.getI1Type());
10747+
Value nonzero = AtenNeScalarOp::create(rewriter, loc, boolType, abs, zero);
10748+
Value none = ConstantNoneOp::create(rewriter, loc);
10749+
rewriter.replaceOpWithNewOp<AtenSumDimIntListOp>(
10750+
op, op.getType(), nonzero, dim, op.getKeepdim(), /*dtype=*/none);
10751+
return success();
10752+
}
10753+
};
10754+
} // namespace
10755+
1068010756
namespace {
1068110757
class DecomposeAtenRandintLowOp : public OpRewritePattern<AtenRandintLowOp> {
1068210758
public:
@@ -13842,6 +13918,7 @@ class DecomposeComplexOpsPass
1384213918
addPatternIfTargetOpIsIllegal<DecomposeAtenMseLossOp>(patterns);
1384313919
addPatternIfTargetOpIsIllegal<DecomposeAtenL1LossOp>(patterns);
1384413920
addPatternIfTargetOpIsIllegal<DecomposeAtenNormScalarOptDimOp>(patterns);
13921+
addPatternIfTargetOpIsIllegal<DecomposeAtenLinalgVectorNormOp>(patterns);
1384513922
addPatternIfTargetOpIsIllegal<DecomposeAtenRandintOp>(patterns);
1384613923
addPatternIfTargetOpIsIllegal<DecomposeAtenRandintLowOp>(patterns);
1384713924
addPatternIfTargetOpIsIllegal<DecomposeAtenVarMeanCorrectionOp>(patterns);

projects/pt1/python/torch_mlir_e2e_test/test_suite/reduction.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,6 +2069,149 @@ def ReduceLN3NormModule_basic(module, tu: TestUtils):
20692069
# ==============================================================================
20702070

20712071

2072+
class ReduceLInfNormModule(torch.nn.Module):
2073+
def __init__(self):
2074+
super().__init__()
2075+
2076+
@export
2077+
@annotate_args(
2078+
[
2079+
None,
2080+
([-1], torch.float32, True),
2081+
]
2082+
)
2083+
def forward(self, a):
2084+
return torch.linalg.vector_norm(a, dim=0, ord=float("inf"))
2085+
2086+
2087+
@register_test_case(module_factory=lambda: ReduceLInfNormModule())
2088+
def ReduceLInfNormModule_basic(module, tu: TestUtils):
2089+
module.forward(tu.rand(5))
2090+
2091+
2092+
# ==============================================================================
2093+
2094+
2095+
class ReduceLNegInfNormModule(torch.nn.Module):
2096+
def __init__(self):
2097+
super().__init__()
2098+
2099+
@export
2100+
@annotate_args(
2101+
[
2102+
None,
2103+
([-1], torch.float32, True),
2104+
]
2105+
)
2106+
def forward(self, a):
2107+
return torch.linalg.vector_norm(a, dim=0, ord=float("-inf"))
2108+
2109+
2110+
@register_test_case(module_factory=lambda: ReduceLNegInfNormModule())
2111+
def ReduceLNegInfNormModule_basic(module, tu: TestUtils):
2112+
module.forward(tu.rand(5))
2113+
2114+
2115+
# ==============================================================================
2116+
2117+
2118+
class ReduceL0NormModule(torch.nn.Module):
2119+
def __init__(self):
2120+
super().__init__()
2121+
2122+
@export
2123+
@annotate_args(
2124+
[
2125+
None,
2126+
([-1], torch.float32, True),
2127+
]
2128+
)
2129+
def forward(self, a):
2130+
return torch.linalg.vector_norm(a, dim=0, ord=0)
2131+
2132+
2133+
@register_test_case(module_factory=lambda: ReduceL0NormModule())
2134+
def ReduceL0NormModule_basic(module, tu: TestUtils):
2135+
# Include exact zeros so the nonzero-count semantics are exercised.
2136+
module.forward(torch.tensor([0.0, 1.5, 0.0, -2.0, 3.0]))
2137+
2138+
2139+
# ==============================================================================
2140+
2141+
2142+
# ord = +inf over a single dim of a multidim input, with keepdim.
2143+
class ReduceLInfNormKeepDimModule(torch.nn.Module):
2144+
def __init__(self):
2145+
super().__init__()
2146+
2147+
@export
2148+
@annotate_args(
2149+
[
2150+
None,
2151+
([-1, -1], torch.float32, True),
2152+
]
2153+
)
2154+
def forward(self, a):
2155+
return torch.linalg.vector_norm(a, dim=1, keepdim=True, ord=float("inf"))
2156+
2157+
2158+
@register_test_case(module_factory=lambda: ReduceLInfNormKeepDimModule())
2159+
def ReduceLInfNormKeepDimModule_basic(module, tu: TestUtils):
2160+
module.forward(tu.rand(3, 4))
2161+
2162+
2163+
# ==============================================================================
2164+
2165+
2166+
# ord = -inf reducing over all dims (dim=None) of a multidim input.
2167+
class ReduceLNegInfNormNoneDimModule(torch.nn.Module):
2168+
def __init__(self):
2169+
super().__init__()
2170+
2171+
@export
2172+
@annotate_args(
2173+
[
2174+
None,
2175+
([-1, -1], torch.float32, True),
2176+
]
2177+
)
2178+
def forward(self, a):
2179+
return torch.linalg.vector_norm(a, dim=None, ord=float("-inf"))
2180+
2181+
2182+
@register_test_case(module_factory=lambda: ReduceLNegInfNormNoneDimModule())
2183+
def ReduceLNegInfNormNoneDimModule_basic(module, tu: TestUtils):
2184+
module.forward(tu.rand(3, 4))
2185+
2186+
2187+
# ==============================================================================
2188+
2189+
2190+
# ord = 0 over multiple dims, with keepdim, of a multidim input.
2191+
class ReduceL0NormMultiDimKeepDimModule(torch.nn.Module):
2192+
def __init__(self):
2193+
super().__init__()
2194+
2195+
@export
2196+
@annotate_args(
2197+
[
2198+
None,
2199+
([-1, -1], torch.float32, True),
2200+
]
2201+
)
2202+
def forward(self, a):
2203+
return torch.linalg.vector_norm(a, dim=[0, 1], keepdim=True, ord=0)
2204+
2205+
2206+
@register_test_case(module_factory=lambda: ReduceL0NormMultiDimKeepDimModule())
2207+
def ReduceL0NormMultiDimKeepDimModule_basic(module, tu: TestUtils):
2208+
# Include exact zeros so the nonzero-count semantics are exercised.
2209+
module.forward(torch.tensor([[0.0, 1.5, 0.0], [-2.0, 0.0, 3.0]]))
2210+
2211+
2212+
# ==============================================================================
2213+
2214+
20722215
class ReduceL3NormAllDimsModule(torch.nn.Module):
20732216
def __init__(self):
20742217
super().__init__()

0 commit comments

Comments
 (0)