bio_img_matlab

The MATLAB Blog

Practical Advice for People on the Leading Edge

New in MATLAB: Single precision sparse matrices

Sparse matrices have been in MATLAB for a long time but, up until now, the only types of sparse matrices you could create were double or logical sparse matrices.
This is no longer the case and one of the thousands of new features in MATLAB R2025a is the fact that you can now create and use single precision sparse matrices. For example, here is how you create a random single precision sparse matrix of size 50 x 100 with density 0.001.
mySparseSingleMat = sprand(50,100,0.001,"single")
mySparseSingleMat = 50×100 sparse single matrix (5 nonzeros)
(39,4) 0.7142 (46,67) 0.9240 (4,69) 0.8103 (37,74) 0.5364 (33,89) 0.0137
The "single" argument is new and is also available for several other sparse matrix creation functions such as sparse, speye, spalloc, sprandn and sprandsym. As with previous versions, if you don't specify any type then the default is double but you can now also be explicit if you prefer.
mySpeyeSingleMat = speye(5,"double")
mySpeyeSingleMat = 5×5 sparse double matrix (5 nonzeros)
(1,1) 1 (2,2) 1 (3,3) 1 (4,4) 1 (5,5) 1
Support for single precision sparse matrices has been on many people's wish-lists for a long time and its great news that we now have them. In the remainder of this post I'm going to dig into some of the details about why single precision sparse matrices are useful, why it was a lot of work to add them to MATLAB and a couple of tips about best practice when using them.

Single precision sparse matrices save memory

A double precision number requires 64-bits (8 bytes) to store it while a single precision number uses 32-bits (4 bytes). So, the first benefit of using a single sparse matrix is that it requires less memory compared to the double version but at the cost of reduced precision. Given this information, your first guess might be that a single sparse matrix requires half the space of a double sparse matrix, which is the case for dense matrices. If so, your first guess is wrong
mySparseDoubleMat = sprand(50,100,0.001,"double")
mySparseDoubleMat = 50×100 sparse double matrix (5 nonzeros)
(27,5) 0.8934 (45,5) 0.4957 (45,39) 0.3892 (31,68) 0.2711 (11,81) 0.1672
We use the whos function to determine how much memory this matrix is using
whos mySparseDoubleMat
Name Size Bytes Class Attributes mySparseDoubleMat 50x100 888 double sparse
Let's now create a single sparse matrix with the same size and with the same number of non-zero elements
mySparseSingleMat = sprand(50,100,0.001,"single")
mySparseSingleMat = 50×100 sparse single matrix (5 nonzeros)
(20,6) 0.2901 (44,19) 0.7310 (19,76) 0.2183 (34,85) 0.7405 (22,92) 0.9392
Again, we show how much memory this is using
whos mySparseSingleMat
Name Size Bytes Class Attributes mySparseSingleMat 50x100 868 single sparse
The double sparse matrix is using 888 bytes while single sparse is using 868 bytes -- a saving of only 20 bytes! The reason for this is that along with the actual values of the non-zeros, sparse matrices have to store their locations. The memory required for location storage of the non-zeros is the same no matter what precision the actual values are.
In the two examples given above, the memory required for the storage of the locations of the non-zeros is identical. The difference between them is simply that the 5 non-zeros are stored as double in the first case (and hence taking up 40 bytes) and single in the second case (and hence using 20 bytes) which gives us the 20 byte difference between the two.
Both are significantly better than the 50 x 100 x 8 = 40,000 bytes for the dense double matrix or 50 x 100 x 4 = 20,000 bytes for the dense single matrix. For dense matrices, a zero takes up just as much memory space as any other number. Sparse matrices can save a lot of memory and single sparse matrices can save even more, particularly when dealing with very large matrices with a large number of non-zero elements. For example, consider this sparse double matrix
largeDoubleSparse = sprand(10000,10000,0.1,"double");
fprintf("This double matrix has %d non-zeros\n",nnz(largeDoubleSparse))
This double matrix has 9515142 non-zeros
Let's see how much memory this matrix is using
whos largeDoubleSparse
Name Size Bytes Class Attributes largeDoubleSparse 10000x10000 152322280 double sparse
A single precision version of the same matrix is about 25% smaller in memory, saving about 36 Megabytes in this instance.
largeSingleSparse = single(largeDoubleSparse);
whos largeSingleSparse
Name Size Bytes Class Attributes largeSingleSparse 10000x10000 114261712 single sparse

Operations with single sparse matrices can be faster

Roughly speaking, since we have fewer bytes to compute when operating on single precision numbers, we can expect to compute things more quickly compared to double precision. Iterative computations are often faster too since we have fewer decimal places of precision to worry about.
As an example, let's create a large double sparse random matrix and time how long it takes to find the largest 6 singular values using svds
rng(5); % Seed the random number generator for reproducibility
randDoubleSparse = sprand(40000,10000,0.0005,"double");
tic
svds(randDoubleSparse,6,"largest")
ans = 6×1
5.9095 4.2673 4.2507 4.2486 4.2456 4.2381
doubleSvdsTime = toc
doubleSvdsTime = 1.7272
Now I'll cast the large random matrix to single precision so that the only difference between these two matrices is the precision.
randSingleSparse = single(randDoubleSparse);
I perform the svds calculation on the result
tic
svds(randSingleSparse,6,"largest")
ans = 6×1 single column vector
5.9095 4.2673 4.2507 4.2486 4.2456 4.2381
singleSvdsTime = toc
singleSvdsTime = 1.2296
Which is a very nice speed-up!
fprintf("The speed-up gained from using single precision was %f times\n",doubleSvdsTime/singleSvdsTime)
The speed-up gained from using single precision was 1.404657 times
The primary reason for this is that the tolerance is reduced for single inputs. svds sets the default tolerance to 1e-5 instead of 1e-10 because 1e-10 is not reachable in single-precision. This is something you should always bear in mind when using single precision for anything; yes, it can be faster but you always pay the cost of reduced precision.
The reduction in precision isn't the only reason why this operation is faster though. Let's request the exact same tolerance in both double and single cases
tic; svds(randDoubleSparse,6,"largest",Tolerance=1e-4); toc
Elapsed time is 1.215458 seconds.
single is still faster.
tic; svds(randSingleSparse,6,"largest",Tolerance=1e-4); toc
Elapsed time is 0.866802 seconds.
Several other iterative solvers such as eigs, pcg, minres and gmres might also be faster when using single sparse matrices for similar reasons but I haven't investigated.

Matrix-vector products are faster too

An example of a non-iterative algorithm where moving to single sparse gives a performance boost is multiplying a sparse matrix by a dense vector. Even for large matrices, this is a fast operation so I'll do it 100 times and time that.
First in double precision
randDoubleSparse = sprand(80000,80000,0.0006,"double");
randDoubleVector = rand(80000,1,"double");
tic
for repeats = 1:100
singleSquared = randDoubleSparse * randDoubleVector;
end
toc
Elapsed time is 1.995894 seconds.
and now in single precision
randSingleSparse = sprand(80000,80000,0.0006,"single");
randSingleVector = rand(80000,1,"single");
tic
for repeats = 1:100
singleSquared = randSingleSparse * randSingleVector;
end
toc
Elapsed time is 0.703923 seconds.
Again, this is another nice speed-up.
Speaking of speed....

Single Sparse works on GPUs and distributed arrays as well

As part of a cross-team collaboration, the Parallel Computing Toolbox team ensured that you can use single precision sparse matrices on GPUs too. You can transfer single sparse matrices created earlier from main memory to the GPU using gpuArray
gpuSparse = gpuArray(mySparseSingleMat)
gpuSparse = 50×100 sparse gpuArray single matrix (5 nonzeros) (20,6) 0.2901 (44,19) 0.7310 (19,76) 0.2183 (34,85) 0.7405 (22,92) 0.9392
or you can create them directly in GPU memory
R = gpuArray.sprand(50,100,0.001,"single")
R = 50×100 sparse gpuArray single matrix (5 nonzeros) (49,12) 0.2728 (32,49) 0.8346 (28,72) 0.3244 (27,74) 0.8721 (7,85) 0.8113
All GPU functions that can compute with double sparse have also had single sparse enabled.
Let's transfer those 80,000 x 80,000 sparse matrices I created earlier onto the GPU
randDoubleSparseGpu = gpuArray(randDoubleSparse);
randSingleSparseGpu = gpuArray(randSingleSparse);
and time how long it takes to multiply them together using gputimeit. I'll also time how long it takes to do this on the CPU for comparison
singleCPUTime = timeit(@() randSingleSparse * randSingleSparse);
doubleCPUTime = timeit(@() randDoubleSparse * randDoubleSparse);
 
singleGpuTime = gputimeit(@() randSingleSparseGpu * randSingleSparseGpu);
doubleGpuTime = gputimeit(@() randDoubleSparseGpu * randDoubleSparseGpu);
fprintf("The CPU did the sparse double matrix multiplication in %.3fs\n" + ...
"The CPU did the sparse single matrix multiplication in %.3fs\n\n" + ...
"The GPU did the sparse double matrix multiplication in %.3fs\n" + ...
"The GPU did the sparse single matrix multiplication in %.3fs\n" ...
,doubleCPUTime, singleCPUTime ,doubleGpuTime,singleGpuTime);
The CPU did the sparse double matrix multiplication in 4.733s The CPU did the sparse single matrix multiplication in 2.888s The GPU did the sparse double matrix multiplication in 0.674s The GPU did the sparse single matrix multiplication in 0.391s
Not only is the GPU version of this computation much faster than the CPU version on my machine but you also see a speed-up when moving from double to single as you'd expect.
Similarly, the team has added supported for Distributed Arrays so now you can create and use single precision sparse matrices that are so big you need to distribute them over multiple nodes of a High Performance Computing (HPC) cluster.

Is single sparse one new feature or many new features?

While putting together this blog post, I found 4 entries in the Release Notes related to single sparse matrices in R2025a. The first entry was in MATLAB itself and referred to the fact that we now have them. Two entries were for the GPU and Distributed Array support discussed above from Parallel Computing Toolbox and one entry referred to calling MATLAB from C++ since single-precision sparse matrices work there too now!
That one entry in the MATLAB release notes mentioned the 6 creation functions that have been updated: sparse, speye, spalloc, sprand, sprandn and sprandsym. If I were the lead developer of this tranche of work I'd be arguing that I had at least 6 new features and not one.
Actually, If I were the lead developer of this, I'd be arguing that this is dozens of new features because all functions in MATLAB that support double-precision sparse matrices including elementary math, linear algebra, and sparse matrix functions now also support single precision sparse matrices. All of those functions had to be modified, tested and get updated documentation.
It's an astonishing amount of work.
I think that this is also an illustration of why different people in MathWorks can give different feature counts when talking about new releases. Like all the truths we cling to, it depends on your point of view.

More informative display for sparse matrices

Another feature that hasn't even been mentioned in the release notes is the improved display of sparse matrices. In R2024a and earlier, it looked like this
sparse([0 0;0 0.2])
ans =
(2,2) 0.2000
compared to the more informative
sparse([0 0;0 0.2])
ans = 2×2 sparse double matrix (1 nonzero)
(2,2) 0.2000
This was actually done in R2024b but it's even more meaningful now that single precision sparse matrices are possible.

What happens when we open MAT files containing single sparse matrices in old versions of MATLAB

If you save a single-precision sparse matrix to a MAT file, you can load it into an earlier release of MATLAB. However, how the data is loaded depends on the release of MATLAB:
  • MATLAB R2021b through R2024b — MATLAB warns that the data has an unsupported data type and loads it as a double-precision sparse matrix.
% This line was run in R2024b
load("from25a.mat")
Warning: Sparse matrix of unsupported datatype. Matrix has been converted to double.
  • MATLAB R2021a and earlier — MATLAB loads the data as a single-precision sparse matrix, but you cannot use the matrix because its data type is unsupported. Attempting to use the matrix can result in undefined behavior.

Style tip: Use "single" and not 'single' when creating single sparse matrices

MATLAB old-timers like me are often guilty of using character arrays such as 'single' when we really should get with the times and use strings, i.e. "single". We seem to get away with it too
smallSparse = sprand(50,100,0.001,'single') % Don't use 'single' like this
smallSparse = 50×100 sparse single matrix (5 nonzeros)
(22,28) 0.8777 (42,71) 0.0741 (24,80) 0.9256 (37,82) 0.1663 (18,89) 0.7241
We really should be using strings though!
smallSparse = sprand(50,100,0.001,"single") % Using "single" is much better
smallSparse = 50×100 sparse single matrix (5 nonzeros)
(14,33) 0.8020 (44,43) 0.8440 (46,63) 0.3696 (4,81) 0.9557 (16,98) 0.7799
One good reason for this is so that the operation will fail more gracefully in older versions of MATLAB. Obviously, specifying the type of a sparse will fail in older versions of MATLAB since it wasn't supported but which type of failure would you prefer here?
% Run this line in R2024b
smallSparse = sprand(50,100,0.001,"single")
Error using <=
Comparison between string and double is not supported.
Error in sprand (line 61)
if ~(rc <= 1 && rc >= 0 && imag(rc) == 0)
We get an error message. Admittedly its a little confusing but since we couldn't go back in time and change this to something like "Choosing the data type for sparse is not supported" its a lot better than the alternative:
%Run this line in R2024b
smallSparse = sprand(50,100,0.001,'single')
smallSparse =
(34,41) 103 (22,42) 110 (40,49) 105 (6,69) 101 (32,73) 115 (35,96) 108
When this first happened to me, it took a little bit of thought to figure out what was going on. When I say 'thought' I mean 'looking at the documentation' which explains the situation. There's a version of sprand that accepts 4 input arguments:
R = sprand(m,n,density,rc) creates a matrix that also has reciprocal condition number approximately equal to rc.
The matrix R is constructed from a sum of matrices of rank one.
If you give MATLAB a character array where it is expecting a numerical array, it converts it to its ASCII numerical equivalent:
double('single')
ans = 1×6
115 105 110 103 108 101
This then gets used as the 4th input argument to sprand and weirdness ensues.
The fact that using "single" instead of 'single' is useful here was not a design decision. It's just a concrete reason I discovered while writing this post to prefer the newer syntax.
My advice is always to use strings instead of character arrays whenever you can. It's newer and tends to be where development focuses its attention on these days.

Will you be using single precision sparse matrices?

Many users have been asking for this functionality for a long time. Are you one of them? Will you be using these features? What is your application? Tell us all about it in the comments section.
|
  • print

Comments

To leave a comment, please click here to sign in to your MathWorks Account or create a new one.