Repository navigation
[FEA]: Faster initialization time for cuda.core abstractions #658
Description
Is this a duplicate?
- I confirmed there appear to be no duplicate issues for this request and that I agree to the Code of Conduct
Area
cuda.core
Is your feature request related to a problem? Please describe.
As mentioned in a previous issue, equivalent operations using CuPy can be significantly faster. In this issue, I am requesting that the initialization of cuda.core abstractions have less overhead. Specifically, when compared to their CuPy counterparts, the initialization of Device, Stream, and Event abstractions are slower.
>>> timeit.timeit('cp.cuda.Device()', setup='import cupy as cp')
0.06881106700166129
>>> timeit.timeit('device = ccx.Device()', setup='import cuda.core.experimental as ccx')
0.5686513699911302
>>> timeit.timeit('cp.cuda.Stream()', setup='import cupy as cp')
1.0035127629962517
>>> timeit.timeit('device.create_stream()', setup='import cuda.core.experimental as ccx; device = ccx.Device(); device.set_current()')
5.299269804003416
>>> timeit.timeit('cp.cuda.Event()', setup='import cupy as cp')
0.393913417996373
>>> timeit.timeit('device.create_event()', setup='import cuda.core.experimental as ccx; device = ccx.Device(); device.set_current()')
3.1525100879953243
This has caused noticeable performance regressions in nvmath-python when transitioning from cupy.cuda to cuda.core for our benchmarks for smaller/medium size arrays or on faster devices where python overhead is more significant.
Specifically, we currently use event recording frequently in order to autoselect algorithms/plans and to wait for computation to complete before returning to the user (for host APIs), and we frequently use the Device constructor to check the current device.
Describe the solution you'd like
These init functions should just as fast as or faster than CuPy's abstractions.
Describe alternatives you've considered
- Refactoring our internal implementation to call
Device()less often by passing around oneDeviceand being careful about context switching. - Using less event recording? Trying to reuse the same two
Events. I don't think this is feasible.
Additional context
I doesn't seem like the best long term solution for nvmath-python to try to work around these issues.
Activity
leofang commentedon May 24, 2025
We need to look at the nvmath-python perf issue more closely. Can you please share how you got the conclusion that
cuda.coreis the cause? Also, which version ofcuda.bindingsdid you install? As mentioned in the meeting 12.9.0 contains quite some perf improvements.It is known that the
Deviceconstructor can be made faster (#460), which could in turn make other things faster too, however the timing we're discussing here are all on the O(100) ns level on my machine, whereas the regression I heard during the meeting is about ~50 us IIRC. It is unclear to me if we're looking at the right bottleneck.leofang commentedon May 26, 2025
As noted in the OP, event creation is much slower than CuPy:
vs
FWIW CuPy's timing is probably simpler to explain in a hand-waving way: It has ~200 ns spent in creating a cdef class object + ~500 ns on the actual CUDA call:
Looking into what's happening on the
cuda.coreside reveals some interesting surprises. For example, because we track the CUDA context in Python, we need to retrieve it before crating an event, and the retrieval alone is already taking ~1 usAmong the ~1 us, here's a rough breakdown: about ~600 ns in the actual work
and the remaining ~400 ns in spent on this
precondition(timing a decorator is a bit hard, the easiest is to check the event creation time with/without it):cuda-python/cuda_core/cuda/core/experimental/_device.py
Line 1058 in ed884c9
which we copied from cuQuantum Python to nvmath-python to
cuda.core. So about 600 ns are used by helper functions (handle_return&precondition).I have also looked into the remaining ~3 us in creating an event. Similar to the above finding we have time consumed here and there in Python easily. It is simple to get a sense of this through line_profiler (but for hotspot analysis like this the exact timings reported by it cannot be trusted):
So about 24% of time spent on creating a destructor (
_mnff), 15% oncheck_or_create_options(it again came from cuQuantum Python to nvmath-python to here), 43% on creating the event itself.It seems to me we are paying the technical debts of ignoring small problem sizes. However, the above analysis still does not answer "why do we see a ~50 us overhead in nvmath-python?" AFAIK there's only one event creation per
execute()(what's benchmarked in nvmath) and it should not be so costly. It'd be better to understand it first.leofang commentedon May 26, 2025
FWIW the stream creation performance is a lot closer
carterbox commentedon May 27, 2025
Our investigation started when Satya noticed our performance benchmarks (timed matmuls of various sizes) were slower for the upcoming release.
I then stepped through the git history since v0.3 looking at when the performance benchmark decreased. The performance drops were all correlated with the replacement of cupy abstractions with cuda.core abstractions.
Finally, I used a statistical profiler to measure where the hot spots were during the running of these benchmarks both from the previous release and in the current version. Since I was using a statistical profiler, I increased the number of benchmarking trials from 10 to 100 so that small overheads would be captured more often. I also disabled the test with autotune since I didn't care to benchmark the autotuning process. Using a host-side python profiler is fine for this case since we are trying measure the Python overhead. The benchmarks for current and previous release were run in the exact same environment, so all the device-side computation is the same.
Used the latest releases of cuda-python as of May 23, 2025.
When comparing the call trees in the profiling results, many are similar. When they aren't similar, we follow the call stack down and find a cuda.core abstraction.
I cannot publish the profiling results for an unreleased version of nvmath-python, so... check internally.
carterbox commentedon May 30, 2025
One obvious difference between torch/cupy and cuda.core is that these high level constructs are implemented in cython (torch/cupy) instead of python (cuda.core). Is migrating these classes to cython on the roadmap? Do we have any evidence which would suggest what kind of performance benefit we would get?
leofang commentedon Jun 4, 2025
@emcastillo is looking into this
emcastillo commentedon Jun 4, 2025
Check #677 to see the actual bottlenecks :)
leofang commentedon Jun 30, 2025
Summarizing some findings based on which an optimization plan is made.
@emcastillo ran

pytest -s tests/nvmath_tests/linalg/advanced/matmul/test_perf.pyfrom nvmath-python and produced the flame charts for nvmath-python beta 3 (v0.3.0) and beta 4 (v0.4.0). Below is an overlay for easier comparison (the blue column is a call to cuBLASLt and should span the same time regardless of nvmath-python versions):Below is a list of functions that Emilio and I found costly (see also #677):
handle_returncheck_or_create_options)_MembersNeededForFinalizepreconditionEmilio pushed #677, and we should consider it as "what's the best perf we can get." But #677 is not something we can ship easily today, because it'd require us building cuda.core against different CTK major versions and ship
cuda-core-cuXXsimilar to most other Python projects do. #709 is a compromised alternative to stay CTK-version-agnostic while getting reasonable performance boost by lowering several of the listed Python functions to Cython.