Skip to content

[MLIR][Presburger] Implement function to evaluate the number of terms in a generating function. #78078

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

Merged
merged 45 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1ec4bd3
Initial commit
Abhinav271828 Jan 14, 2024
f286b7b
Bug fix
Abhinav271828 Jan 14, 2024
19ade35
Add test
Abhinav271828 Jan 14, 2024
39dd6a8
Minor fixes
Abhinav271828 Jan 14, 2024
64a55bc
Use const
Abhinav271828 Jan 14, 2024
b3331b2
Push down declarations
Abhinav271828 Jan 14, 2024
f78ef76
Mark getters as const
Abhinav271828 Jan 14, 2024
06ca87a
Fix doc
Abhinav271828 Jan 14, 2024
92a73fc
Abstract convolution
Abhinav271828 Jan 14, 2024
aad6ca2
Abstract out mu substitution
Abhinav271828 Jan 14, 2024
5205765
Fix doc
Abhinav271828 Jan 14, 2024
fc0dba7
Fix doc
Abhinav271828 Jan 14, 2024
d3e758e
Fix doc
Abhinav271828 Jan 14, 2024
ac6b038
Fix doc
Abhinav271828 Jan 14, 2024
94fe370
Fix build
Abhinav271828 Jan 14, 2024
fd7879a
Reorg
Abhinav271828 Jan 14, 2024
819ec57
Typo
Abhinav271828 Jan 14, 2024
eeb020c
Reorg doc
Abhinav271828 Jan 14, 2024
0bda772
Abstract out binomial coefficient computation
Abhinav271828 Jan 14, 2024
e811dfd
Fix conv
Abhinav271828 Jan 14, 2024
3b40fb5
Fix conv
Abhinav271828 Jan 14, 2024
0ef2159
Fix doc
Abhinav271828 Jan 14, 2024
f20bb63
Fix doc
Abhinav271828 Jan 14, 2024
98ae71e
Fix doc
Abhinav271828 Jan 14, 2024
3ab2f48
Improve simplify()
Abhinav271828 Jan 16, 2024
8b2c962
Implement collectTerms()
Abhinav271828 Jan 16, 2024
aa10c1f
Add test for computeNumTerms()
Abhinav271828 Jan 16, 2024
5ebc95d
Fix doc
Abhinav271828 Jan 17, 2024
e6fae1d
Fix doc
Abhinav271828 Jan 17, 2024
b387fbf
Add assert
Abhinav271828 Jan 17, 2024
afefb1b
Fix test
Abhinav271828 Jan 19, 2024
ca40b83
Add accessors
Abhinav271828 Jan 19, 2024
64bf738
Fix doc
Abhinav271828 Jan 19, 2024
0c261a1
Move convolution
Abhinav271828 Jan 20, 2024
b609445
Refactor accessors
Abhinav271828 Jan 21, 2024
02c2ce3
Fix docs
Abhinav271828 Jan 21, 2024
65baf07
Minor fixes
Abhinav271828 Jan 21, 2024
5289ffd
FIx normalize
Abhinav271828 Jan 21, 2024
ff5f967
Fix doc
Abhinav271828 Jan 21, 2024
b919d7c
Fix doc
Abhinav271828 Jan 21, 2024
66bc0d2
Rename conv
Abhinav271828 Jan 21, 2024
44d9fc5
Fix test
Abhinav271828 Jan 21, 2024
8c7e15b
Remove extra element from conv
Abhinav271828 Jan 21, 2024
f809eae
Conv test
Abhinav271828 Jan 21, 2024
92d5937
Minor fixes
Abhinav271828 Jan 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions mlir/include/mlir/Analysis/Presburger/Barvinok.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ QuasiPolynomial getCoefficientInRationalFunction(unsigned power,
ArrayRef<QuasiPolynomial> num,
ArrayRef<Fraction> den);

/// Find the number of terms in a generating function, as
/// a quasipolynomial in the parameter space of the input function.
/// The generating function must be such that for all values of the
/// parameters, the number of terms is finite.
QuasiPolynomial computeNumTerms(const GeneratingFunction &gf);

} // namespace detail
} // namespace presburger
} // namespace mlir
Expand Down
10 changes: 6 additions & 4 deletions mlir/include/mlir/Analysis/Presburger/GeneratingFunction.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@ class GeneratingFunction {
#endif // NDEBUG
}

unsigned getNumParams() { return numParam; }
unsigned getNumParams() const { return numParam; }

SmallVector<int> getSigns() { return signs; }
SmallVector<int> getSigns() const { return signs; }

std::vector<ParamPoint> getNumerators() { return numerators; }
std::vector<ParamPoint> getNumerators() const { return numerators; }

std::vector<std::vector<Point>> getDenominators() { return denominators; }
std::vector<std::vector<Point>> getDenominators() const {
return denominators;
}

GeneratingFunction operator+(GeneratingFunction &gf) const {
assert(numParam == gf.getNumParams() &&
Expand Down
7 changes: 6 additions & 1 deletion mlir/include/mlir/Analysis/Presburger/QuasiPolynomial.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,14 @@ class QuasiPolynomial : public PresburgerSpace {
QuasiPolynomial operator*(const QuasiPolynomial &x) const;
QuasiPolynomial operator/(const Fraction x) const;

// Removes terms which evaluate to zero from the expression.
// Removes terms which evaluate to zero from the expression
// and folds affine functions which are constant into the
// constant coefficients.
QuasiPolynomial simplify();

// Group together like terms in the expression.
QuasiPolynomial collectTerms();

Fraction getConstantTerm();

private:
Expand Down
5 changes: 5 additions & 0 deletions mlir/include/mlir/Analysis/Presburger/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,11 @@ SmallVector<MPInt, 8> getComplementIneq(ArrayRef<MPInt> ineq);
/// The vectors must have the same sizes.
Fraction dotProduct(ArrayRef<Fraction> a, ArrayRef<Fraction> b);

/// Find the product of two polynomials, each given by an array of
/// coefficients.
std::vector<Fraction> multiplyPolynomials(ArrayRef<Fraction> a,
ArrayRef<Fraction> b);

} // namespace presburger
} // namespace mlir

Expand Down
239 changes: 239 additions & 0 deletions mlir/lib/Analysis/Presburger/Barvinok.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//

#include "mlir/Analysis/Presburger/Barvinok.h"
#include "mlir/Analysis/Presburger/Utils.h"
#include "llvm/ADT/Sequence.h"
#include <algorithm>

Expand Down Expand Up @@ -245,3 +246,241 @@ QuasiPolynomial mlir::presburger::detail::getCoefficientInRationalFunction(
}
return coefficients[power].simplify();
}

/// Substitute x_i = t^μ_i in one term of a generating function, returning
/// a quasipolynomial which represents the exponent of the numerator
/// of the result, and a vector which represents the exponents of the
/// denominator of the result.
/// If the returned value is {num, dens}, it represents the function
/// t^num / \prod_j (1 - t^dens[j]).
/// v represents the affine functions whose floors are multiplied by the
/// generators, and ds represents the list of generators.
std::pair<QuasiPolynomial, std::vector<Fraction>>
substituteMuInTerm(unsigned numParams, ParamPoint v, std::vector<Point> ds,
Point mu) {
unsigned numDims = mu.size();
for (const Point &d : ds)
assert(d.size() == numDims &&
"μ has to have the same number of dimensions as the generators!");

// First, the exponent in the numerator becomes
// - (μ • u_1) * (floor(first col of v))
// - (μ • u_2) * (floor(second col of v)) - ...
// - (μ • u_d) * (floor(d'th col of v))
// So we store the negation of the dot products.

// We have d terms, each of whose coefficient is the negative dot product.
SmallVector<Fraction> coefficients;
coefficients.reserve(numDims);
for (const Point &d : ds)
coefficients.push_back(-dotProduct(mu, d));

// Then, the affine function is a single floor expression, given by the
// corresponding column of v.
ParamPoint vTranspose = v.transpose();
std::vector<std::vector<SmallVector<Fraction>>> affine;
affine.reserve(numDims);
for (unsigned j = 0; j < numDims; ++j)
affine.push_back({SmallVector<Fraction>(vTranspose.getRow(j))});

QuasiPolynomial num(numParams, coefficients, affine);
num = num.simplify();

std::vector<Fraction> dens;
dens.reserve(ds.size());
// Similarly, each term in the denominator has exponent
// given by the dot product of μ with u_i.
for (const Point &d : ds) {
// This term in the denominator is
// (1 - t^dens.back())
dens.push_back(dotProduct(d, mu));
}

return {num, dens};
}

/// Normalize all denominator exponents `dens` to their absolute values
/// by multiplying and dividing by the inverses, in a function of the form
/// sign * t^num / prod_j (1 - t^dens[j]).
/// Here, sign = ± 1,
/// num is a QuasiPolynomial, and
/// each dens[j] is a Fraction.
void normalizeDenominatorExponents(int &sign, QuasiPolynomial &num,
std::vector<Fraction> &dens) {
// We track the number of exponents that are negative in the
// denominator, and convert them to their absolute values.
unsigned numNegExps = 0;
Fraction sumNegExps(0, 1);
for (unsigned j = 0, e = dens.size(); j < e; ++j) {
if (dens[j] < 0) {
numNegExps += 1;
sumNegExps += dens[j];
}
}

// If we have (1 - t^-c) in the denominator, for positive c,
// multiply and divide by t^c.
// We convert all negative-exponent terms at once; therefore
// we multiply and divide by t^sumNegExps.
// Then we get
// -(1 - t^c) in the denominator,
// increase the numerator by c, and
// flip the sign of the function.
if (numNegExps % 2 == 1)
sign = -sign;
num = num - QuasiPolynomial(num.getNumInputs(), sumNegExps);
}

/// Compute the binomial coefficients nCi for 0 ≤ i ≤ r,
/// where n is a QuasiPolynomial.
std::vector<QuasiPolynomial> getBinomialCoefficients(QuasiPolynomial n,
unsigned r) {
unsigned numParams = n.getNumInputs();
std::vector<QuasiPolynomial> coefficients;
coefficients.reserve(r + 1);
coefficients.push_back(QuasiPolynomial(numParams, 1));
for (unsigned j = 1; j <= r; ++j)
// We use the recursive formula for binomial coefficients here and below.
coefficients.push_back(
(coefficients[j - 1] * (n - QuasiPolynomial(numParams, j - 1)) /
Fraction(j, 1))
.simplify());
return coefficients;
}

/// Compute the binomial coefficients nCi for 0 ≤ i ≤ r,
/// where n is a QuasiPolynomial.
std::vector<Fraction> getBinomialCoefficients(Fraction n, Fraction r) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes more sense for r to be an integer here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it already is; not sure why it showed you Fraction 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it still says that r is an argument with type Fraction

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry my mistake

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is Fraction because we need to call it as getBinomialCoefficients(den + 1, den + 1)

std::vector<Fraction> coefficients;
coefficients.reserve((int64_t)floor(r));
coefficients.push_back(1);
for (unsigned j = 1; j <= r; ++j)
coefficients.push_back(coefficients[j - 1] * (n - (j - 1)) / (j));
return coefficients;
}

/// We have a generating function of the form
/// f_p(x) = \sum_i sign_i * (x^n_i(p)) / (\prod_j (1 - x^d_{ij})
///
/// where sign_i is ±1,
/// n_i \in Q^p -> Q^d is the sum of the vectors d_{ij}, weighted by the
/// floors of d affine functions on p parameters.
/// d_{ij} \in Q^d are vectors.
///
/// We need to find the number of terms of the form x^t in the expansion of
/// this function.
/// However, direct substitution (x = (1, ..., 1)) causes the denominator
/// to become zero.
///
/// We therefore use the following procedure instead:
/// 1. Substitute x_i = (s+1)^μ_i for some vector μ. This makes the generating
/// function a function of a scalar s.
/// 2. Write each term in this function as P(s)/Q(s), where P and Q are
/// polynomials. P has coefficients as quasipolynomials in d parameters, while
/// Q has coefficients as scalars.
/// 3. Find the constant term in the expansion of each term P(s)/Q(s). This is
/// equivalent to substituting s = 0.
///
/// Verdoolaege, Sven, et al. "Counting integer points in parametric
/// polytopes using Barvinok's rational functions." Algorithmica 48 (2007):
/// 37-66.
QuasiPolynomial
mlir::presburger::detail::computeNumTerms(const GeneratingFunction &gf) {
// Step (1) We need to find a μ such that we can substitute x_i =
// (s+1)^μ_i. After this substitution, the exponent of (s+1) in the
// denominator is (μ_i • d_{ij}) in each term. Clearly, this cannot become
// zero. Hence we find a vector μ that is not orthogonal to any of the
// d_{ij} and substitute x accordingly.
std::vector<Point> allDenominators;
for (ArrayRef<Point> den : gf.getDenominators())
allDenominators.insert(allDenominators.end(), den.begin(), den.end());
Point mu = getNonOrthogonalVector(allDenominators);

unsigned numParams = gf.getNumParams();
const std::vector<std::vector<Point>> &ds = gf.getDenominators();
QuasiPolynomial totalTerm(numParams, 0);
for (unsigned i = 0, e = ds.size(); i < e; ++i) {
int sign = gf.getSigns()[i];

// Compute the new exponents of (s+1) for the numerator and the
// denominator after substituting μ.
auto [numExp, dens] =
substituteMuInTerm(numParams, gf.getNumerators()[i], ds[i], mu);
// Now the numerator is (s+1)^numExp
// and the denominator is \prod_j (1 - (s+1)^dens[j]).

// Step (2) We need to express the terms in the function as quotients of
// polynomials. Each term is now of the form
// sign_i * (s+1)^numExp / (\prod_j (1 - (s+1)^dens[j]))
// For the i'th term, we first normalize the denominator to have only
// positive exponents. We convert all the dens[j] to their
// absolute values and change the sign and exponent in the numerator.
normalizeDenominatorExponents(sign, numExp, dens);

// Then, using the formula for geometric series, we replace each (1 -
// (s+1)^(dens[j])) with
// (-s)(\sum_{0 ≤ k < dens[j]} (s+1)^k).
for (unsigned j = 0, e = dens.size(); j < e; ++j)
dens[j] = abs(dens[j]) - 1;
// Note that at this point, the semantics of `dens[j]` changes to mean
// a term (\sum_{0 ≤ k ≤ dens[j]} (s+1)^k). The denominator is, as before,
// a product of these terms.

// Since the -s are taken out, the sign changes if there is an odd number
// of such terms.
unsigned r = dens.size();
if (dens.size() % 2 == 1)
sign = -sign;

// Thus the term overall now has the form
// sign'_i * (s+1)^numExp /
// (s^r * \prod_j (\sum_{0 ≤ k < dens[j]} (s+1)^k)).
// This means that
// the numerator is a polynomial in s, with coefficients as
// quasipolynomials (given by binomial coefficients), and the denominator
// is a polynomial in s, with integral coefficients (given by taking the
// convolution over all j).

// Step (3) We need to find the constant term in the expansion of each
// term. Since each term has s^r as a factor in the denominator, we avoid
// substituting s = 0 directly; instead, we find the coefficient of s^r in
// sign'_i * (s+1)^numExp / (\prod_j (\sum_k (s+1)^k)),
// Letting P(s) = (s+1)^numExp and Q(s) = \prod_j (...),
// we need to find the coefficient of s^r in P(s)/Q(s),
// for which we use the `getCoefficientInRationalFunction()` function.

// First, we compute the coefficients of P(s), which are binomial
// coefficients.
// We only need the first r+1 of these, as higher-order terms do not
// contribute to the coefficient of s^r.
std::vector<QuasiPolynomial> numeratorCoefficients =
getBinomialCoefficients(numExp, r);

// Then we compute the coefficients of each individual term in Q(s),
// which are (dens[i]+1) C (k+1) for 0 ≤ k ≤ dens[i].
std::vector<std::vector<Fraction>> eachTermDenCoefficients;
std::vector<Fraction> singleTermDenCoefficients;
eachTermDenCoefficients.reserve(r);
for (const Fraction &den : dens) {
singleTermDenCoefficients = getBinomialCoefficients(den + 1, den + 1);
eachTermDenCoefficients.push_back(
ArrayRef<Fraction>(singleTermDenCoefficients).slice(1));
}

// Now we find the coefficients in Q(s) itself
// by taking the convolution of the coefficients
// of all the terms.
std::vector<Fraction> denominatorCoefficients;
denominatorCoefficients = eachTermDenCoefficients[0];
for (unsigned j = 1, e = eachTermDenCoefficients.size(); j < e; ++j)
denominatorCoefficients = multiplyPolynomials(denominatorCoefficients,
eachTermDenCoefficients[j]);

totalTerm =
totalTerm + getCoefficientInRationalFunction(r, numeratorCoefficients,
denominatorCoefficients) *
QuasiPolynomial(numParams, sign);
}

return totalTerm.simplify();
}
47 changes: 46 additions & 1 deletion mlir/lib/Analysis/Presburger/QuasiPolynomial.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,18 @@ QuasiPolynomial QuasiPolynomial::operator/(const Fraction x) const {
return qp;
}

// Removes terms which evaluate to zero from the expression.
// Removes terms which evaluate to zero from the expression and
// integrate affine functions which are constants into the
// coefficients.
QuasiPolynomial QuasiPolynomial::simplify() {
Fraction newCoeff = 0;
SmallVector<Fraction> newCoeffs({});

std::vector<SmallVector<Fraction>> newAffineTerm({});
std::vector<std::vector<SmallVector<Fraction>>> newAffine({});

unsigned numParam = getNumInputs();

for (unsigned i = 0, e = coefficients.size(); i < e; i++) {
// A term is zero if its coefficient is zero, or
if (coefficients[i] == Fraction(0, 1))
Expand All @@ -114,9 +122,46 @@ QuasiPolynomial QuasiPolynomial::simplify() {
});
if (product_is_zero)
continue;

// Now, we know the term is nonzero.

// We now eliminate the affine functions which are constant
// by merging them into the coefficients.
newAffineTerm = {};
newCoeff = coefficients[i];
for (ArrayRef<Fraction> term : affine[i]) {
bool allCoeffsZero = llvm::all_of(
term.slice(0, numParam), [](const Fraction c) { return c == 0; });
if (allCoeffsZero)
newCoeff *= term[numParam];
else
newAffineTerm.push_back(SmallVector<Fraction>(term));
}

newCoeffs.push_back(newCoeff);
newAffine.push_back(newAffineTerm);
}
return QuasiPolynomial(getNumInputs(), newCoeffs, newAffine);
}

QuasiPolynomial QuasiPolynomial::collectTerms() {
SmallVector<Fraction> newCoeffs({});
std::vector<std::vector<SmallVector<Fraction>>> newAffine({});

for (unsigned i = 0, e = affine.size(); i < e; i++) {
bool alreadyPresent = false;
for (unsigned j = 0, f = newAffine.size(); j < f; j++) {
if (affine[i] == newAffine[j]) {
newCoeffs[j] += coefficients[i];
alreadyPresent = true;
}
}
if (alreadyPresent)
continue;
newCoeffs.push_back(coefficients[i]);
newAffine.push_back(affine[i]);
}

return QuasiPolynomial(getNumInputs(), newCoeffs, newAffine);
}

Expand Down
Loading