Skip to content

Refactor Hilbert_basis() and replace a slow test #40387

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 5 commits into from
Jul 25, 2025

Conversation

orlitzky
Copy link
Contributor

@orlitzky orlitzky commented Jul 8, 2025

Initially my goal was to replace one slow Hilbert_basis() test, but I did some refactoring along the way -- none of which really affects the performance of the Hilbert basis calculation. The commit message lists these changes.

To replace the test, I made up some examples and fed them to normaliz. If the sage answer agrees with the normaliz answer, they must both be right, right?

@orlitzky orlitzky requested review from vbraun and user202729 July 8, 2025 10:19
@orlitzky orlitzky force-pushed the simpler-hilbert-basis branch from da05285 to 3542b29 Compare July 8, 2025 11:50
@mantepse
Copy link
Contributor

mantepse commented Jul 8, 2025

I don't understand this. On my computer, in a fresh sage, current develop branch, I have

sage: cone = Cone([[1,2,3,4], [0,1,0,7], [3,1,0,2], [0,0,1,0]]).dual()
sage: timeit("cone.Hilbert_basis()")
5 loops, best of 3: 45.6 ns per loop

Are you saying that this is too slow? Or does this depend on certain packages which may or may not be installed?

@orlitzky
Copy link
Contributor Author

orlitzky commented Jul 8, 2025

I don't understand this. On my computer, in a fresh sage, current develop branch, I have

sage: cone = Cone([[1,2,3,4], [0,1,0,7], [3,1,0,2], [0,0,1,0]]).dual()
sage: timeit("cone.Hilbert_basis()")
5 loops, best of 3: 45.6 ns per loop

Are you saying that this is too slow? Or does this depend on certain packages which may or may not be installed?

The result is cached so running it in a loop and picking the fastest one makes it look very fast indeed. Runs 2,3,... are instantaneous.

When I run sage -t --long, this test raises a warning because it takes about 40s measured in CPU time. Your CPU may fare better (until we can normalize the CPU time, #33022) but in any case this test is relatively slow and does not (AFAIK, which is not very far) test anything that other faster examples cannot test.

@mantepse
Copy link
Contributor

mantepse commented Jul 8, 2025

I don't understand this. On my computer, in a fresh sage, current develop branch, I have

sage: cone = Cone([[1,2,3,4], [0,1,0,7], [3,1,0,2], [0,0,1,0]]).dual()
sage: timeit("cone.Hilbert_basis()")
5 loops, best of 3: 45.6 ns per loop

Are you saying that this is too slow? Or does this depend on certain packages which may or may not be installed?

The result is cached so running it in a loop and picking the fastest one makes it look very fast indeed. Runs 2,3,... are instantaneous.

Oh, you are absolutely right. However, ...

When I run sage -t --long, this test raises a warning because it takes about 40s measured in CPU time. Your CPU may fare better (until we can normalize the CPU time, #33022) but in any case this test is relatively slow and does not (AFAIK, which is not very far) test anything that other faster examples cannot test.

... I find this hard to believe:

sage: cone = Cone([[1,2,3,4], [0,1,0,7], [3,1,0,2], [0,0,1,0]]).dual()
sage: timeit("cone.Hilbert_basis()", number=1, repeat=1)
1 loop, best of 1: 1.49 s per loop

This would mean that my CPU would be 30 times faster. cat /proc/cpuinfo says Intel(R) Core(TM) Ultra 5 125H. Is this such a good computer?

else:
# Avoid the PointCollection overhead if nothing was
# added to the irreducible list beyond self.rays().
return self.rays()

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
else:
# Avoid the PointCollection overhead if nothing was
# added to the irreducible list beyond self.rays().
return self.rays()
# Avoid the PointCollection overhead if nothing was
# added to the irreducible list beyond self.rays().
return self.rays()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, thanks. I also made a similar change at the beginning of the function. Defining L = () unconditionally does not take any time and lets us eliminate one branch of the if/else.

@mantepse
Copy link
Contributor

mantepse commented Jul 8, 2025

One tiny change that seems to make a slight difference is

diff --git a/src/sage/geometry/cone.py b/src/sage/geometry/cone.py
index bcaa578c339..d8aed5e222e 100644
--- a/src/sage/geometry/cone.py
+++ b/src/sage/geometry/cone.py
@@ -1685,7 +1685,7 @@ class ConvexRationalPolyhedralCone(IntegralRayCollection, Container, ConvexSet_c
         need_strict = region.endswith("interior")
         M = self.dual_lattice()
         for c in self._PPL_cone().minimized_constraints():
-            pr = M(c.coefficients()) * point
+            pr = M(*(c.coefficients())) * point
             if c.is_equality():
                 if pr != 0:
                     return False

It may be possible to speed up this line further, or perhaps ToricLattice_generic.__call__ by bypassing some checks, but I don't know enough about this.

@orlitzky orlitzky force-pushed the simpler-hilbert-basis branch from 0abdf6a to 9dda8a0 Compare July 9, 2025 12:43
@orlitzky
Copy link
Contributor Author

orlitzky commented Jul 9, 2025

One tiny change that seems to make a slight difference is...

I tried this but the results were inconsistent, it gets slower as the lattice gets bigger. I let myself get carried away and spent a few hours trying to optimize this method last night. I was only able to obtain a very small improvement by reorganizing the conditionals inside of the loop (see the latest commit). The speedup is consistent though.

@orlitzky
Copy link
Contributor Author

orlitzky commented Jul 9, 2025

... I find this hard to believe:
...
This would mean that my CPU would be 30 times faster. cat /proc/cpuinfo says Intel(R) Core(TM) Ultra 5 125H. Is this such a good computer?

A factor of 30x is not outrageous. Keep in mind that timeit() is measuring wall time and not CPU time. Wall time is inherently unreliable -- you can slow it down by watching a movie on your PC at the same time the test is running, speed it up by running the code in parallel, etc. The "slow test" warnings use CPU time to avoid some of that inconsistency, but CPU time can still vary without a normalizing factor (that no one has gotten around to yet).

CPU time will be affected by the options used to compile sage and its dependencies, the vector features (SSE, AVX, etc.) of the CPU, hardware mitigations for spectre and meltdown, and some other internal details of the processor. I have an old Core 2 Duo Thinkpad with all of the vulnerability mitigations turned on, and this test takes about 28s on it. The computer where it takes 40s is actually brand new, but it has 64 processors with the trade-off being that each of them is individually pretty slow.

So long as it leads to useful refactorings and performance improvements I'm not too worried about accidentally speeding up a test that might not technically be considered slow once we have the normalizing factor.

orlitzky added 3 commits July 9, 2025 09:14
Some refactorings to the implementation of Hilbert_basis():

  * Construct a cone L from our linear_subspace() so that "y in L"
    works as intended (currently we try to coerce y into a vector
    space in a try/except block). This is not any faster, but it
    makes the code easier to read.

  * Remove the irreducibles from "gens" as we construct it.

  * Negate a condition in a loop to avoid bailing out with "continue"
    as part of the normal control flow.

  * Use a boolean indicator to check if the list of irreducibles
    was modified, rather than recomputing its length.
We have one Hilbert_basis() test that is raising "slow test!" warnings
at around 40s. Here we replace it with three tests, each of which runs
relatively quickly. The trio completes in about 15s.

Since I know very little about Hilbert bases, I have checked the
results using Normaliz. For example,

  $ cat cone.in
  amb_space 4
  cone 4
  1 0 1 0
  -1 0 1 0
  0 1 1 0
  0 -1 1 0
  $ normaliz --HilbertBasis cone
  $ cat cone.out
  ...

(the resulting basis is written to cone.out).
Tighten the whitespace around list/generator comprehensions, and
simplify the control flow in two instances by eliminating an if/else
branch. (Only one of these was suggested by the reviewer, but the
other is in a similar spirit.) Thanks to Martin Rubey for the
suggestions.
@mantepse
Copy link
Contributor

mantepse commented Jul 9, 2025

One tiny change that seems to make a slight difference is...

I tried this but the results were inconsistent, it gets slower as the lattice gets bigger.

Ah, you are right! Calling a function with many arguments is more expensive.

@mantepse
Copy link
Contributor

mantepse commented Jul 9, 2025

Interestingly, the new version is about 10% slower on my computer on the example above :-(

Edit: Hm, not sure. develop is the same speed, so it's probably noise.

@orlitzky orlitzky force-pushed the simpler-hilbert-basis branch from 9dda8a0 to 0d254be Compare July 9, 2025 13:38
@orlitzky
Copy link
Contributor Author

orlitzky commented Jul 9, 2025

If you use %timeit -c (built-in to ipython) instead of sage's own timeit(), it will use the CPU time and show you the standard deviation as well as the mean. It's a bit more reliable.

@mantepse
Copy link
Contributor

mantepse commented Jul 9, 2025

The following seems to make a big difference, but is, apparently, not correct - there are failing tests in fan_morphism which I do not understand.

diff --git a/src/sage/geometry/cone.py b/src/sage/geometry/cone.py
index c34bc9242a9..377da5e6216 100644
--- a/src/sage/geometry/cone.py
+++ b/src/sage/geometry/cone.py
@@ -1684,8 +1684,9 @@ class ConvexRationalPolyhedralCone(IntegralRayCollection, Container, ConvexSet_c
             return False
         need_strict = region.endswith("interior")
         M = self.dual_lattice()
+        E = M.element_class
         for c in self._PPL_cone().minimized_constraints():
-            pr = M(c.coefficients()) * point
+            pr = E(M, [ZZ(e) for e in c.coefficients()]) * point
             if pr < 0:
                 return False
             elif pr > 0:

return False
elif pr > 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
elif pr > 0:
if pr > 0:

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 have dropped this commit as well for now. It turns out that the new version can be slower in some pathological cases (like the empty cone in a big space) where most of the constraints are equality and the equality constraints are listed first. It might be possible to work around, but I have other things I should be doing instead of trying to shave nanoseconds off of this method :)

@orlitzky orlitzky force-pushed the simpler-hilbert-basis branch from 0d254be to 90a51fc Compare July 9, 2025 19:55
@orlitzky
Copy link
Contributor Author

orlitzky commented Jul 9, 2025

The following seems to make a big difference, but is, apparently, not correct - there are failing tests in fan_morphism which I do not understand.

Quotient lattices have a different element_class even though they may have the same number of coordinates. The M() constructor does some work to dispatch correctly.

orlitzky added 2 commits July 9, 2025 19:01
When constructing the subcone that represents the given cone's
linear_subspace(), we don't need to check that the generators are
valid or minimal -- fewer generators might work, but no subset will
work.
When the cone K has no rays, the test "x in K" can be done quickly by
checking if x is zero. This can be a significant improvement if the
lattice is large, and risks wasting only as much time as it takes to
compare an integer to zero (i.e. nothing compared to how long the rest
of the containment test is going to take).
@orlitzky
Copy link
Contributor Author

orlitzky commented Jul 9, 2025

Two more small improvements:

  • The Cone() in Hilbert_basis() can use check=False because we know that the generators are good.
  • I noticed that _contains() does not have a special case for the trivial cone (the set {0}). This can be checked instantaneously and can save a lot of time when the lattice is large.

@mantepse
Copy link
Contributor

Looks good, the failures seem to be unrelated. Thank you for your patience!

@orlitzky
Copy link
Contributor Author

Thank you for the careful review!

vbraun pushed a commit to vbraun/sage that referenced this pull request Jul 14, 2025
sagemathgh-40387: Refactor Hilbert_basis() and replace a slow test
    
Initially my goal was to replace one slow `Hilbert_basis()` test, but I
did some refactoring along the way -- none of which really affects the
performance of the Hilbert basis calculation. The commit message lists
these changes.

To replace the test, I made up some examples and fed them to normaliz.
If the sage answer agrees with the normaliz answer, they must both be
right, right?
    
URL: sagemath#40387
Reported by: Michael Orlitzky
Reviewer(s): Martin Rubey, Michael Orlitzky
vbraun pushed a commit to vbraun/sage that referenced this pull request Jul 18, 2025
sagemathgh-40387: Refactor Hilbert_basis() and replace a slow test
    
Initially my goal was to replace one slow `Hilbert_basis()` test, but I
did some refactoring along the way -- none of which really affects the
performance of the Hilbert basis calculation. The commit message lists
these changes.

To replace the test, I made up some examples and fed them to normaliz.
If the sage answer agrees with the normaliz answer, they must both be
right, right?
    
URL: sagemath#40387
Reported by: Michael Orlitzky
Reviewer(s): Martin Rubey, Michael Orlitzky
@vbraun vbraun merged commit 9e60de4 into sagemath:develop Jul 25, 2025
20 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants