-
Notifications
You must be signed in to change notification settings - Fork 185
transforms: lower polynomials to arithmetic operations #5951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5323843
6eee718
594db9d
e31e996
0c2d643
a7c6d8d
73054d3
fd27cf2
e6f7a53
2add8e6
bb053d9
ff69cee
80923ba
49cfbf0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| // RUN: xdsl-opt -p polynomial-eval-to-arith %s | filecheck %s | ||
|
|
||
| builtin.module { | ||
| // f64 with domain bounds [-5, 0], degree 2. | ||
| // Scale = 2/(0 - (-5)) = 0.4, offset = -((-5)+0)/(0-(-5)) = 1.0. | ||
| func.func @clenshaw_f64_with_domain(%x: f64) -> f64 { | ||
| %r = polynomial.eval #polynomial.typed_chebyshev_polynomial<[1.000000e+00 : f64, 2.000000e+00 : f64, 3.000000e+00 : f64]> : !polynomial.polynomial<ring = <coefficientType = f64>>, %x {scheme = "clenshaw", domain_lower = -5.000000e+00 : f64, domain_upper = 0.000000e+00 : f64} : f64 | ||
| func.return %r : f64 | ||
| } | ||
|
|
||
| // f64 without domain bounds: skip the affine remap (t = x). | ||
| func.func @clenshaw_f64_no_domain(%x: f64) -> f64 { | ||
| %r = polynomial.eval #polynomial.typed_chebyshev_polynomial<[1.000000e+00 : f64, 2.000000e+00 : f64, 3.000000e+00 : f64]> : !polynomial.polynomial<ring = <coefficientType = f64>>, %x {scheme = "clenshaw"} : f64 | ||
| func.return %r : f64 | ||
| } | ||
|
|
||
| // f32 with domain [-1, 1]: scale=1.0, offset=0.0. | ||
| func.func @clenshaw_f32(%x: f32) -> f32 { | ||
| %r = polynomial.eval #polynomial.typed_chebyshev_polynomial<[5.000000e-01 : f64, 1.200000e+00 : f64, 3.000000e-01 : f64]> : !polynomial.polynomial<ring = <coefficientType = f64>>, %x {scheme = "clenshaw", domain_lower = -1.000000e+00 : f64, domain_upper = 1.000000e+00 : f64} : f32 | ||
| func.return %r : f32 | ||
| } | ||
|
|
||
| // Vector type, with domain bounds, degree 1. | ||
| func.func @clenshaw_vec(%x: vector<4xf32>) -> vector<4xf32> { | ||
| %r = polynomial.eval #polynomial.typed_chebyshev_polynomial<[1.000000e+00 : f64, 2.000000e+00 : f64]> : !polynomial.polynomial<ring = <coefficientType = f64>>, %x {scheme = "clenshaw", domain_lower = -2.000000e+00 : f64, domain_upper = 2.000000e+00 : f64} : vector<4xf32> | ||
| func.return %r : vector<4xf32> | ||
| } | ||
| } | ||
|
|
||
| // CHECK: builtin.module { | ||
|
|
||
| // ===== f64 with domain [-5, 0], coeffs [1, 2, 3] ===== | ||
|
|
||
| // CHECK: func.func @clenshaw_f64_with_domain(%[[X:.*]]: f64) -> f64 { | ||
| // Domain mapping: t = x * 0.4 + 1.0 | ||
| // CHECK-NEXT: %[[SCALE:.*]] = arith.constant 4.000000e-01 : f64 | ||
| // CHECK-NEXT: %[[OFFSET:.*]] = arith.constant 1.000000e+00 : f64 | ||
| // CHECK-NEXT: %[[SCALED:.*]] = arith.mulf %[[X]], %[[SCALE]] : f64 | ||
| // CHECK-NEXT: %[[T:.*]] = arith.addf %[[SCALED]], %[[OFFSET]] : f64 | ||
| // two_t = 2 * t | ||
| // CHECK-NEXT: %[[TWO:.*]] = arith.constant 2.000000e+00 : f64 | ||
| // CHECK-NEXT: %[[TWO_T:.*]] = arith.mulf %[[TWO]], %[[T]] : f64 | ||
| // b_{n+2} = 0, b_{n+1} = 0 | ||
| // CHECK-NEXT: %{{.*}} = arith.constant 0.000000e+00 : f64 | ||
| // CHECK-NEXT: %{{.*}} = arith.constant 0.000000e+00 : f64 | ||
| // 2 Clenshaw iterations | ||
| // CHECK-COUNT-2: arith.addf | ||
| // Final: result = c_0/2 + t * b_1 - b_2 | ||
| // CHECK: %[[C0_HALF:.*]] = arith.constant 5.000000e-01 : f64 | ||
| // CHECK-NEXT: %[[T_B1:.*]] = arith.mulf %[[T]], %{{.*}} : f64 | ||
| // CHECK-NEXT: %[[ADD:.*]] = arith.addf %[[C0_HALF]], %[[T_B1]] : f64 | ||
| // CHECK-NEXT: %[[RES:.*]] = arith.subf %[[ADD]], %{{.*}} : f64 | ||
| // CHECK-NEXT: func.return %[[RES]] : f64 | ||
| // CHECK-NEXT: } | ||
|
|
||
| // ===== f64 without domain bounds: t = x, no scale/offset ops ===== | ||
|
|
||
| // CHECK: func.func @clenshaw_f64_no_domain(%[[XN:.*]]: f64) -> f64 { | ||
| // CHECK-NEXT: %[[TWO_N:.*]] = arith.constant 2.000000e+00 : f64 | ||
| // CHECK-NEXT: %[[TWO_T_N:.*]] = arith.mulf %[[TWO_N]], %[[XN]] : f64 | ||
| // CHECK-NEXT: %{{.*}} = arith.constant 0.000000e+00 : f64 | ||
| // CHECK-NEXT: %{{.*}} = arith.constant 0.000000e+00 : f64 | ||
| // CHECK-COUNT-2: arith.addf | ||
| // CHECK: %[[C0H_N:.*]] = arith.constant 5.000000e-01 : f64 | ||
| // CHECK-NEXT: %[[TB1_N:.*]] = arith.mulf %[[XN]], %{{.*}} : f64 | ||
| // CHECK-NEXT: %{{.*}} = arith.addf %[[C0H_N]], %[[TB1_N]] : f64 | ||
| // CHECK-NEXT: %[[RES_N:.*]] = arith.subf %{{.*}}, %{{.*}} : f64 | ||
| // CHECK-NEXT: func.return %[[RES_N]] : f64 | ||
| // CHECK-NEXT: } | ||
|
|
||
| // ===== f32 with [-1, 1]: coefficients converted to f32 ===== | ||
|
|
||
| // CHECK: func.func @clenshaw_f32(%[[X32:.*]]: f32) -> f32 { | ||
| // CHECK-NEXT: %{{.*}} = arith.constant 1.000000e+00 : f32 | ||
| // CHECK-NEXT: %{{.*}} = arith.constant -0.000000e+00 : f32 | ||
| // CHECK-NEXT: %{{.*}} = arith.mulf %[[X32]], %{{.*}} : f32 | ||
| // CHECK-NEXT: %[[T32:.*]] = arith.addf %{{.*}}, %{{.*}} : f32 | ||
| // CHECK-NEXT: %{{.*}} = arith.constant 2.000000e+00 : f32 | ||
| // CHECK-NEXT: %{{.*}} = arith.mulf %{{.*}}, %[[T32]] : f32 | ||
| // CHECK-NEXT: %{{.*}} = arith.constant 0.000000e+00 : f32 | ||
| // CHECK-NEXT: %{{.*}} = arith.constant 0.000000e+00 : f32 | ||
| // CHECK-COUNT-2: arith.addf | ||
| // CHECK: %{{.*}} = arith.constant 2.500000e-01 : f32 | ||
| // CHECK-NEXT: %{{.*}} = arith.mulf %[[T32]], %{{.*}} : f32 | ||
| // CHECK-NEXT: %{{.*}} = arith.addf %{{.*}}, %{{.*}} : f32 | ||
| // CHECK-NEXT: %[[RES32:.*]] = arith.subf %{{.*}}, %{{.*}} : f32 | ||
| // CHECK-NEXT: func.return %[[RES32]] : f32 | ||
| // CHECK-NEXT: } | ||
|
|
||
| // ===== vector<4xf32> with [-2, 2], degree 1 ===== | ||
| // scale = 0.5, offset = -0.0 (since (lower+upper) == 0). | ||
|
|
||
| // CHECK: func.func @clenshaw_vec(%[[XV:.*]]: vector<4xf32>) -> vector<4xf32> { | ||
| // CHECK-NEXT: %{{.*}} = arith.constant dense<5.000000e-01> : vector<4xf32> | ||
| // CHECK-NEXT: %{{.*}} = arith.constant dense<-0.000000e+00> : vector<4xf32> | ||
| // CHECK-NEXT: %{{.*}} = arith.mulf %[[XV]], %{{.*}} : vector<4xf32> | ||
| // CHECK-NEXT: %[[TV:.*]] = arith.addf %{{.*}}, %{{.*}} : vector<4xf32> | ||
| // CHECK-NEXT: %{{.*}} = arith.constant dense<2.000000e+00> : vector<4xf32> | ||
| // CHECK-NEXT: %{{.*}} = arith.mulf %{{.*}}, %[[TV]] : vector<4xf32> | ||
| // CHECK-NEXT: %{{.*}} = arith.constant dense<0.000000e+00> : vector<4xf32> | ||
| // CHECK-NEXT: %{{.*}} = arith.constant dense<0.000000e+00> : vector<4xf32> | ||
| // 1 Clenshaw iteration (degree=1) | ||
| // CHECK-COUNT-1: arith.addf | ||
| // CHECK: %{{.*}} = arith.constant dense<5.000000e-01> : vector<4xf32> | ||
| // CHECK-NEXT: %{{.*}} = arith.mulf %[[TV]], %{{.*}} : vector<4xf32> | ||
| // CHECK-NEXT: %{{.*}} = arith.addf %{{.*}}, %{{.*}} : vector<4xf32> | ||
| // CHECK-NEXT: %[[RESV:.*]] = arith.subf %{{.*}}, %{{.*}} : vector<4xf32> | ||
| // CHECK-NEXT: func.return %[[RESV]] : vector<4xf32> | ||
| // CHECK-NEXT: } | ||
|
|
||
| // CHECK: } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import pytest | ||
|
|
||
| from xdsl.builder import ImplicitBuilder | ||
| from xdsl.context import Context | ||
| from xdsl.dialects import arith, polynomial, test | ||
| from xdsl.dialects.builtin import Builtin, ModuleOp, f32 | ||
| from xdsl.transforms.polynomial_eval_to_arith import PolynomialEvalToArithPass | ||
| from xdsl.utils.exceptions import PassFailedException | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("scheme", list(polynomial.EvalScheme)) | ||
| def test_every_scheme_has_lowering(scheme: polynomial.EvalScheme): | ||
| """ | ||
| This tests if every member of `EvalScheme` in the polynomial dialect has a defined lowering to arithmetic ops. | ||
| """ | ||
| ctx = Context() | ||
| ctx.load_dialect(Builtin) | ||
| ctx.load_dialect(arith.Arith) | ||
| ctx.load_dialect(polynomial.Polynomial) | ||
| ctx.load_dialect(test.Test) | ||
|
|
||
| module = ModuleOp([]) | ||
| with ImplicitBuilder(module.body): | ||
| x = test.TestOp(result_types=[f32]).results[0] | ||
| polynomial.EvalOp.get( | ||
| value=x, | ||
| coefficients=(1.0, 2.0, 3.0), | ||
| element_type=f32, | ||
| scheme=scheme, | ||
| ) | ||
|
|
||
| try: | ||
| PolynomialEvalToArithPass().apply(ctx, module) | ||
| except PassFailedException as e: | ||
| pytest.fail( | ||
| f"EvalScheme.{scheme.name} has no dispatch branch in " | ||
| f"polynomial-eval-to-arith. Add a case for it in " | ||
| f"PolynomialEvalToArith.match_and_rewrite. ({e})" | ||
| ) | ||
|
Comment on lines
+35
to
+39
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's test this with lit/filecheck instead |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| """ | ||
| Expand `polynomial.eval` ops to arithmetic operations. | ||
|
|
||
| This pass dispatches on the `scheme` attribute and emits the corresponding arith ops. | ||
|
|
||
| Currently supported schemes: | ||
| - "clenshaw": Chebyshev series evaluated via Clenshaw's recurrence. | ||
|
|
||
| """ | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
| from xdsl.context import Context | ||
| from xdsl.dialects import arith, polynomial | ||
| from xdsl.dialects.builtin import ( | ||
| AnyFloat, | ||
| DenseIntOrFPElementsAttr, | ||
| FloatAttr, | ||
| ModuleOp, | ||
| TensorType, | ||
| VectorType, | ||
| ) | ||
| from xdsl.ir import Operation, SSAValue | ||
| from xdsl.irdl import isa | ||
| from xdsl.passes import ModulePass | ||
| from xdsl.pattern_rewriter import ( | ||
| PatternRewriter, | ||
| PatternRewriteWalker, | ||
| RewritePattern, | ||
| op_type_rewrite_pattern, | ||
| ) | ||
| from xdsl.utils.exceptions import PassFailedException | ||
|
|
||
|
|
||
| def _float_constant( | ||
| value: float, | ||
| tp: AnyFloat | VectorType[AnyFloat] | TensorType[AnyFloat], | ||
| rewriter: PatternRewriter, | ||
| ) -> arith.ConstantOp: | ||
| """Create and insert a float constant, handling scalar/vector/tensor types.""" | ||
| if isa(tp, VectorType[AnyFloat]): | ||
| attr = DenseIntOrFPElementsAttr.from_list(tp, [value]) | ||
| elif isa(tp, TensorType[AnyFloat]): | ||
| attr = DenseIntOrFPElementsAttr.from_list(tp, [value]) | ||
| elif isa(tp, AnyFloat): | ||
| attr = FloatAttr(value, tp) | ||
| else: | ||
| raise TypeError(f"Unsupported type for float constant: {tp}") | ||
| return rewriter.insert(arith.ConstantOp(attr)) | ||
|
|
||
|
Comment on lines
+35
to
+50
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this feels familiar, do we have code like this in the framework already? I think there was something similar in your exp to polynomial pass, is that right? I think we might want a helper for this in builtin with dedicated tests.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, what is kind of suboptimal is that the function returns an But I could make a function in but it feels a bit awkward to define another function then in each pass
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah sorry that's what I meant
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think we should do this first before merging this PR |
||
|
|
||
| def expand_clenshaw( | ||
| op: polynomial.EvalOp, | ||
| rewriter: PatternRewriter, | ||
| coeffs: tuple[float, ...], | ||
| lower: float | None, | ||
| upper: float | None, | ||
| ) -> Operation: | ||
| """ | ||
| Expand a Chebyshev series via Clenshaw's recurrence into arith ops. | ||
|
|
||
| If no domain bounds are provided, the input is assumed to already be | ||
| in the canonical Chebyshev domain [-1, 1]. | ||
|
|
||
| Returns the final operation whose result is the evaluation result. | ||
| """ | ||
| x = op.value | ||
| tp = x.type | ||
| if not isa(tp, AnyFloat | VectorType[AnyFloat] | TensorType[AnyFloat]): | ||
| raise TypeError(f"Unsupported type for polynomial.eval expansion: {tp}") | ||
|
|
||
| n = len(coeffs) - 1 | ||
|
|
||
| # --- domain mapping: t = x * scale + offset -------------------------- | ||
| t: SSAValue | ||
| if lower is not None and upper is not None: | ||
| scale = 2.0 / (upper - lower) | ||
| offset = -(upper + lower) / (upper - lower) | ||
| scale_op = _float_constant(scale, tp, rewriter) | ||
| offset_op = _float_constant(offset, tp, rewriter) | ||
| scaled = rewriter.insert(arith.MulfOp(x, scale_op.result)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not essential, but I wonder if this would all be a lot cleaner with an implicit builder? |
||
| t = rewriter.insert(arith.AddfOp(scaled.result, offset_op.result)).result | ||
| else: | ||
| t = x # canonical Chebyshev domain [-1, 1] | ||
|
|
||
| # --- Clenshaw recurrence -------------------------------------------- | ||
| two = _float_constant(2.0, tp, rewriter) | ||
| two_t = rewriter.insert(arith.MulfOp(two.result, t)) | ||
|
|
||
| b_prev2 = _float_constant(0.0, tp, rewriter) # b_{n+2} | ||
| b_prev1 = _float_constant(0.0, tp, rewriter) # b_{n+1} | ||
|
|
||
| for k in range(n, 0, -1): | ||
| c_k = _float_constant(coeffs[k], tp, rewriter) | ||
| two_t_b = rewriter.insert(arith.MulfOp(two_t.result, b_prev1.result)) | ||
| sub = rewriter.insert(arith.SubfOp(two_t_b.result, b_prev2.result)) | ||
| b_k = rewriter.insert(arith.AddfOp(sub.result, c_k.result)) | ||
| b_prev2 = b_prev1 | ||
| b_prev1 = b_k | ||
|
|
||
| # --- final: result = c_0/2 + t * b_1 - b_2 --------------------------- | ||
| c0_half = _float_constant(coeffs[0] / 2.0, tp, rewriter) | ||
| t_b1 = rewriter.insert(arith.MulfOp(t, b_prev1.result)) | ||
| add = rewriter.insert(arith.AddfOp(c0_half.result, t_b1.result)) | ||
| return rewriter.insert(arith.SubfOp(add.result, b_prev2.result)) | ||
|
|
||
|
|
||
| class PolynomialEvalToArith(RewritePattern): | ||
| """Replace each `polynomial.eval` op with the arith ops for its scheme.""" | ||
|
|
||
| @op_type_rewrite_pattern | ||
| def match_and_rewrite( | ||
| self, op: polynomial.EvalOp, rewriter: PatternRewriter | ||
| ) -> None: | ||
| coeffs = op.polynomial.coeff_values | ||
| lower = op.domain_lower.value.data if op.domain_lower is not None else None | ||
| upper = op.domain_upper.value.data if op.domain_upper is not None else None | ||
|
|
||
| scheme = op.eval_scheme | ||
|
|
||
| match scheme: | ||
| case polynomial.EvalScheme.CLENSHAW: | ||
| expanded = expand_clenshaw(op, rewriter, coeffs, lower, upper) | ||
| case _: | ||
| # Verifier already restricts `scheme` to known EvalScheme members, | ||
| # so this is only reachable if a new scheme is added without a | ||
| # corresponding lowering branch. | ||
| raise PassFailedException( | ||
| f"polynomial.eval scheme {scheme.value!r} has no lowering" | ||
| ) | ||
| rewriter.replace_op(op, (), (expanded.results[0],)) | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class PolynomialEvalToArithPass(ModulePass): | ||
| """ | ||
| Expand `polynomial.eval` ops to arithmetic operations. | ||
|
|
||
| All information needed for lowering (coefficients, scheme, domain | ||
| bounds) is read directly from each op, so this pass takes no | ||
| parameters. | ||
| """ | ||
|
|
||
| name = "polynomial-eval-to-arith" | ||
|
|
||
| def apply(self, ctx: Context, op: ModuleOp) -> None: | ||
| PatternRewriteWalker( | ||
| PolynomialEvalToArith(), | ||
| apply_recursively=False, | ||
| ).rewrite_module(op) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ | |
| memref_stream_tile_outer_loops, | ||
| memref_stream_unnest_out_parameters, | ||
| memref_streamify, | ||
| polynomial_eval_to_arith, | ||
| reconcile_unrealized_casts, | ||
| riscv_allocate_registers, | ||
| riscv_lower_parallel_mov, | ||
|
|
@@ -58,6 +59,7 @@ | |
| lower_affine.LowerAffinePass(), | ||
| convert_scf_to_riscv_scf.ConvertScfToRiscvPass(), | ||
| expand_math_to_polynomials.ExpandMathToPolynomialsPass(), | ||
| polynomial_eval_to_arith.PolynomialEvalToArithPass(), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels like it should be a separate change? |
||
| convert_arith_to_riscv_snitch.ConvertArithToRiscvSnitchPass(), | ||
| convert_arith_to_riscv.ConvertArithToRiscvPass(), | ||
| convert_func_to_riscv_func.ConvertFuncToRiscvFuncPass(), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: There's no need to have the
builtin.moduleop explicitly, and not having it means you don't have to indent the entire file.