Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@lorentzenchr
Copy link
Member

Reference Issues/PRs

Fixes #516.

What does this implement/fix? Explain your changes.

This adds a dedicated Cython routine to compute dense_C = sparse_A @ sparse_B.

Any other comments?

@github-actions
Copy link

github-actions bot commented Aug 15, 2025

✔️ Linting Passed

All linting checks passed. Your pull request is in excellent shape! ☀️

Generated for commit: 1a6f645. Link to the linter CI: here

@lorentzenchr
Copy link
Member Author

lorentzenchr commented Aug 15, 2025

A little benchmark

Summary: Roughly up to a factor of 2 as compared to sparse-sparse A @ B, never worse.

bench(n1=100, n2=10_000, n3=100, sparseness=0.2)
dense @ dense               = 0.006206989288330078s
csr @ csr time              = 0.10099363327026367s
sparse_matmul_to_dense time = 0.059606075286865234s

csr @ csc time              = 0.11165523529052734s
sparse_matmul_to_dense time = 0.06802892684936523s

csc @ csr time              = 0.10589003562927246s
sparse_matmul_to_dense time = 0.06432199478149414s

csc @ csc time              = 0.10179805755615234s
sparse_matmul_to_dense time = 0.06096005439758301s
bench(n1=1000, n2=100, n3=1000, sparseness=0.2)
dense @ dense               = 0.004218101501464844s
csr @ csr time              = 0.08667707443237305s
sparse_matmul_to_dense time = 0.041673898696899414s

csr @ csc time              = 0.08884096145629883s
sparse_matmul_to_dense time = 0.041052818298339844s

csc @ csr time              = 0.08859896659851074s
sparse_matmul_to_dense time = 0.040785789489746094s

csc @ csc time              = 0.08806490898132324s
sparse_matmul_to_dense time = 0.07811689376831055s

I guess that the csc @ csc case could be improved by constructing out as F-contiguous, but this would make the contiguity of the returned result dependent on input sparse formats. And this case might be rare.

Details
from time import time
import numpy as np
import scipy.sparse as sp
from sklearn.utils.sparsefuncs import sparse_matmul_to_dense
def bench(n1=100, n2=100, n3=100, sparseness=0.5, rng=123):
    rng = np.random.default_rng(rng)
    a_dense = rng.standard_normal((n1, n2))
    b_dense = rng.standard_normal((n2, n3))
    p = [1 - sparseness, sparseness]
    a_dense.flat[rng.choice([False, True], size=n1 * n2, p=p)] = 0
    b_dense.flat[rng.choice([False, True], size=n2 * n3, p=p)] = 0
    t0 = time()
    a_dense @ b_dense
    t = time() - t0
    print(f"dense @ dense               = {t}s")
    for af in ("csr", "csc"):
        a = sp.csr_array(a_dense) if af == "csr" else sp.csc_array(a_dense)
        for bf in ("csr", "csc"):
            b = sp.csr_array(b_dense) if bf == "csr" else sp.csc_array(b_dense)
            t0 = time()
            a @ b
            t = time() - t0
            print(f"{af} @ {bf} time              = {t}s")
            t0 = time()
            sparse_matmul_to_dense(a, b)
            t = time() - t0
            print(f"sparse_matmul_to_dense time = {t}s\n")

* csr_matmul_csc_to_dense does not improve performance vs. converting to csr
Copy link
Contributor

@OmarManzoor OmarManzoor left a comment

Choose a reason for hiding this comment

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

Thank you for the PR @lorentzenchr. I left a few comments

Copy link
Contributor

@OmarManzoor OmarManzoor left a comment

Choose a reason for hiding this comment

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

A few more

Copy link
Contributor

@OmarManzoor OmarManzoor left a comment

Choose a reason for hiding this comment

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

LGTM. Thank you @lorentzenchr

@OmarManzoor OmarManzoor added the Waiting for Second Reviewer First reviewer is done, need a second one! label Aug 18, 2025
)


def sparse_matmul_to_dense(A, B, out=None):
Copy link
Member

Choose a reason for hiding this comment

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

Do you see using out somewhere out side of testing code?

Copy link
Member Author

Choose a reason for hiding this comment

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

So far, the only call in non-tests is via safe_sparse_dot - on purpose. safe_sparse_dot does not have an out parameter. But there are a few places where it could be used, e.g. in sandwich_dot inside linear_models._linear_loss.py.

n1, n3 = n3, n1
else:
# It seems best to just convert to csr.
A = A.tocsr()
Copy link
Member

Choose a reason for hiding this comment

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

Doe this use more memory than the implementation on main?

(Same question for the B conversion below: B = B.tocsr())

Copy link
Member Author

Choose a reason for hiding this comment

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

Scipy (at least for CSR and CSC) converts the "other" matrix/array to the same format, here https://github.com/scipy/scipy/blob/f762ab3dddf1541da7475580f16d5a4b8da31fea/scipy/sparse/_compressed.py#L432C32-L432C37
Therefore the code of this PR uses the same amount of memory as pure scipy A @ B, only difference is the returned dense array.

@thomasjpfan thomasjpfan merged commit e1021ba into scikit-learn:main Aug 19, 2025
40 checks passed
@lorentzenchr lorentzenchr deleted the sparse_matmul branch August 19, 2025 05:25
lucyleeow pushed a commit to lucyleeow/scikit-learn that referenced this pull request Aug 22, 2025
@jeremiedbb jeremiedbb mentioned this pull request Sep 3, 2025
13 tasks
DeaMariaLeon pushed a commit to DeaMariaLeon/scikit-learn that referenced this pull request Sep 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sparse-sparse and sparse-dense dot product with dense output

3 participants