Skip to content

Port SpBench's boolean SpGEMM (cuBool/nsparse hash accumulator) as sp… - #21

Open
laesse10 wants to merge 2 commits into
mainfrom
spbench-spgemm
Open

Port SpBench's boolean SpGEMM (cuBool/nsparse hash accumulator) as sp…#21
laesse10 wants to merge 2 commits into
mainfrom
spbench-spgemm

Conversation

@laesse10

Copy link
Copy Markdown
Collaborator

…gemm_hash

SpBench (github.com/EgorOrachyov/SpBench) benchmarks boolean sparse x and + across cuBool, CUSP, cuSPARSE and SuiteSparse. Only cuBool carries its own mathematics -- the rest are library calls -- so the port follows cuBool_MxM down into its vendored nsparse fork, where the product is a five-phase, row-binned hash accumulator rather than any kind of gemm.

nsys on a GH200 (300 iterations each of web-Google and roadNet-CA, CUDA 12.9, sm_90) puts 87% of MxM's wall time on the device and splits that device time 50.6% across the two hash passes, 34.2% across the two bin histogram+scatter passes and 13.4% on the row analysis -- no kernel above 23%, so the boundary is the whole pipeline, not its top symbol. The CUB scan/sort leaves are 1.5%.

The port keeps the algorithm that ranking describes: product upper bound per row, binning into (0, 32] ... (2048, 4096] tables, symbolic hash count, exclusive scan, re-binning on exact nnz, numeric hash + bitonic sort + compact. Its docstring names the three simplifications (no global-row path, no accumulate-into-C, sequential order within a bin).

Validated at S against a set-union oracle and an independent dense-accumulator Gustavson transcription, on rectangular 97x53x131 axes, and -- outside pytest, which has no GPU -- against what cuBool's CUDA backend actually produced for luxembourg_osm and roadNet-CA: identical row pointers and identical column indices, 393,261 and 12,908,450 nonzeros.

That comparison needed two fixes to cuBool first; both are upstream bugs, not build breakage, and are reported separately.

…gemm_hash

SpBench (github.com/EgorOrachyov/SpBench) benchmarks boolean sparse `x` and `+`
across cuBool, CUSP, cuSPARSE and SuiteSparse. Only cuBool carries its own
mathematics -- the rest are library calls -- so the port follows cuBool_MxM down
into its vendored nsparse fork, where the product is a five-phase, row-binned
hash accumulator rather than any kind of gemm.

nsys on a GH200 (300 iterations each of web-Google and roadNet-CA, CUDA 12.9,
sm_90) puts 87% of MxM's wall time on the device and splits that device time
50.6% across the two hash passes, 34.2% across the two bin histogram+scatter
passes and 13.4% on the row analysis -- no kernel above 23%, so the boundary is
the whole pipeline, not its top symbol. The CUB scan/sort leaves are 1.5%.

The port keeps the algorithm that ranking describes: product upper bound per
row, binning into (0, 32] ... (2048, 4096] tables, symbolic hash count,
exclusive scan, re-binning on exact nnz, numeric hash + bitonic sort + compact.
Its docstring names the three simplifications (no global-row path, no
accumulate-into-C, sequential order within a bin).

Validated at S against a set-union oracle and an independent dense-accumulator
Gustavson transcription, on rectangular 97x53x131 axes, and -- outside pytest,
which has no GPU -- against what cuBool's CUDA backend actually produced for
luxembourg_osm and roadNet-CA: identical row pointers and identical column
indices, 393,261 and 12,908,450 nonzeros.

That comparison needed two fixes to cuBool first; both are upstream bugs, not
build breakage, and are reported separately.
…rontend refusal

Two things the first commit got wrong.

The hash table was declared ONCE and reused by every row. Upstream gives each row its own
shared-memory table -- one per thread block, or one 32-slot slice per 4-thread pwarp group --
so rows carry no dependence at all; a single hoisted table invents a loop-carried dependence
the library does not have and hands an optimizer a kernel it cannot parallelize without
first undoing the port. Declaring it inside the row loop is what upstream's __shared__
actually is. np.empty rather than np.zeros because the next loop overwrites [0, ts) with the
sentinel anyway, and nothing ever reads past it; the reference pays 15% for privatisation
(400 -> 460 ms at S) instead of 15x. The docstring now says which phases are parallel, which
are atomic histograms, and which is a scan, so the next reader does not have to infer it.

The kernel also fails tests/test_dace_numeric_agreement.py, and it is not this kernel's
defect: simplify's scalar_to_symbol promotes an integer scalar and remove_symbol_indirection
then sympifies a name that resolves to a function. Reduced against the pinned dace a4740d4e7
to one loop, one branch and no arrays beyond the arguments; crc16 and dfa are already listed
for the same message. Added to NUMERIC_BAD as parse_fail, next to them, with the repro. The
parse-only gate (REFUSED) still says ok, so nothing changes there.

Verified after the change: numpy, numba (validation SUCCESS), c/cpp/fortran at fp64 AND
fp32, jax; the union oracle, the independent Gustavson transcription and the 97x53x131
rectangular case; tree/yaml/levels; every pre-commit hook.
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.

1 participant