Skip to content
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

Miscellaneous housekeeping improvements #513

Merged
merged 15 commits into from
Aug 2, 2023

Conversation

aarmey
Copy link
Contributor

@aarmey aarmey commented Jul 22, 2023

Apologies that this does not involve just one change. This resolves some various minor inconsistencies, deprecations, and performance issues:

  • In both the PLSR and HALS implementations, this reduces indexing which improves performance, particularly for Jax and TensorFlow
  • Some methods were not being exported or had missing arguments in their docstrings
  • Most backends provide the np.lstsq interface, so all four return values are passed on now
  • As suggested in Improving the test suite #448, this adds pytest-randomly. This sets the random seeds of the default random generators and reports it in output. I've found this is very helpful when dealing with inconsistent errors.
  • Python v3.8 is now EOL, so I removed some code ensuring its support. The default Python version is now 3.11 in testing.

I will further annotate each of the changes so that this is simpler to review.

@aarmey aarmey self-assigned this Jul 22, 2023
ones,
zeros,
zeros_like,
eye,
where,
conj,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These methods were not being exported, and I didn't see a reason for being left out.

@codecov
Copy link

codecov bot commented Jul 22, 2023

Codecov Report

Merging #513 (b5161a7) into main (94c2ff2) will increase coverage by 0.07%.
Report is 3 commits behind head on main.
The diff coverage is 90.62%.

@@            Coverage Diff             @@
##             main     #513      +/-   ##
==========================================
+ Coverage   86.71%   86.78%   +0.07%     
==========================================
  Files         120      118       -2     
  Lines        7535     7515      -20     
==========================================
- Hits         6534     6522      -12     
+ Misses       1001      993       -8     
Files Changed Coverage Δ
tensorly/__init__.py 100.00% <ø> (ø)
tensorly/utils/__init__.py 100.00% <ø> (ø)
tensorly/tenalg/proximal.py 67.68% <71.42%> (+0.23%) ⬆️
tensorly/backend/core.py 67.67% <80.00%> (-0.01%) ⬇️
tensorly/backend/__init__.py 89.83% <100.00%> (ø)
tensorly/base.py 95.83% <100.00%> (ø)
tensorly/decomposition/_cp.py 83.71% <100.00%> (-0.11%) ⬇️
tensorly/decomposition/_tr.py 85.24% <100.00%> (ø)
tensorly/decomposition/tests/test_cmtf_als.py 100.00% <100.00%> (ø)
tensorly/metrics/entropy.py 86.36% <100.00%> (-0.60%) ⬇️
... and 4 more

... and 1 file with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

"""
raise NotImplementedError

@staticmethod
def min(tensor):
def min(tensor, axis=None):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The axis argument is actively used, so it should be represented here.

from . import backend as tl
from .utils import prod
Copy link
Contributor Author

Choose a reason for hiding this comment

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

prod was only here for Python v3.8 support.

@@ -426,8 +426,6 @@ def parafac(
tl.solve(tl.conj(tl.transpose(pseudo_inverse)), tl.transpose(mttkrp))
)
factors[mode] = factor
if normalize_factors and mode != modes_list[-1]:
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 the decision to do this only at the end came out of #264, but was in a closed PR. I need to dig it up.

K = 8
M = 7
K = 16
M = 14
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To help with occasional testing failures.

# Normalize separately—this was profiling very slow in parafac
Z_comp = cp_normalize(Z_comp_CP)[1]
for mode in range(len(Z_comp)):
factor = multi_mode_dot(Z, Z_comp, skip=mode)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tucker is equivalent to CP when rank 1. This is more performant, and this spot was consistently one of the slowest parts of testing, particularly for indexing-slow backends.

Copy link
Member

Choose a reason for hiding this comment

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

What was the main bottleneck in cp_normalize? Was it just the part making sure the scales are non-zero?
Or was the slow part just calling the CP where it's not needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here, I think this mostly just came down to calling CP, with a lot of extra code contained within, when it is not strictly needed. This call is inside two loops, and so it can be called thousands of times. Within the profiling, I believe most of the time was spent on the MTTKRP, but other parts of the CP call weren't negligible.

)
V = tl.index_update(V, tl.index[k, :], V[k, :] + deltaV)
# Modifying the function for sparsification
if sparsity_coefficient is not None:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code should be unchanged, but (1) reuses some indexing results for indexing-slow backend performance, and (2) I find it a bit more readable by removing repetitive segments and breaking the expressions up into logical units.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, this is much more readable and concise

tl.index[i, j, k],
tl.sum(product * tl.eye(product.shape[0])),
)
tensor = tl.einsum("iaj,jbk,kci->abc", *factors)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have trace in the backend now (see TODO comment), but I thought this was cleaner.

@aarmey aarmey marked this pull request as ready for review July 22, 2023 19:47
Copy link
Member

@JeanKossaifi JeanKossaifi left a comment

Choose a reason for hiding this comment

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

Thanks @aarmey! Always great to have improvements and more readable code!

# Normalize separately—this was profiling very slow in parafac
Z_comp = cp_normalize(Z_comp_CP)[1]
for mode in range(len(Z_comp)):
factor = multi_mode_dot(Z, Z_comp, skip=mode)
Copy link
Member

Choose a reason for hiding this comment

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

What was the main bottleneck in cp_normalize? Was it just the part making sure the scales are non-zero?
Or was the slow part just calling the CP where it's not needed?

@@ -216,7 +216,7 @@ def tensor_ring_als(

if ls_solve == "lstsq":
# Solve least squares problem directly
sol, _ = tl.lstsq(design_mat, tensor_unf)
sol = tl.lstsq(design_mat, tensor_unf)[0]
Copy link
Member

Choose a reason for hiding this comment

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

Is it better to do this? If so why not *_?

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 didn't know *_ was possible! Agreed this will be more readable.

)
V = tl.index_update(V, tl.index[k, :], V[k, :] + deltaV)
# Modifying the function for sparsification
if sparsity_coefficient is not None:
Copy link
Member

Choose a reason for hiding this comment

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

Agreed, this is much more readable and concise

@@ -407,21 +407,21 @@ def test_lstsq():
# test dimensions
a = T.randn((m, n))
b = T.randn((m, k))
x, res = T.lstsq(a, b)
assert_equal(x.shape, (n, k))
ret = T.lstsq(a, b)
Copy link
Member

Choose a reason for hiding this comment

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

Same question as above for those: I find it typically slightly more readible to explicitly catch return values or use _ rather than index tuples

@aarmey aarmey merged commit a982cf9 into tensorly:main Aug 2, 2023
9 of 10 checks passed
@aarmey aarmey deleted the fix-exports-plsr-perf branch October 30, 2023 21:58
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.

None yet

2 participants