I am trying to speed up a vectorized numpy operation by Cythonizing it (or atleast see if I can). The code calculates some sort of stress given two distance matrices (target_distances and map_distances computed from some flattened coordinate vector) and some information about distance types (it can range from 0 to 3 but for my first attempt I have taken it to be all 0 in this case). The numpy version is pycalculus.py:
import scipy
import numpy as np
from scipy.spatial import distance
def decay(x, s=10):
return scipy.special.expit(s*x)
def stress(z, target_distances, dim, distance_types, step,
nrows, ncols):
row_coords = np.reshape(z[:dim*nrows],(nrows,dim))
col_coords = np.reshape(z[dim*nrows:dim*(nrows+ncols)],(ncols,dim))
map_distances = distance.cdist(row_coords, col_coords).copy()
error = target_distances - map_distances
I0 = (distance_types==0) | (distance_types==3)
I2 = distance_types == 2
return np.sum(error[I0]**2) + np.sum((error[I2] + step)**2*decay(error[I2] + step))
In the cythonized version, my pyx file is as follows (calculus.pyx)
import cython
cimport cython
from libc.stdlib cimport malloc, free
cdef extern from "ctools.h":
double stress (double*, double**, int**, double, int, int, int)
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
def stress_cython(double[:] z, double[:,:] target_distances, int [:,:]
distance_types, double step, int dim, int nrows, int ncols):
cdef int i
cdef int j
cdef int N = (nrows+ncols)*dim
z_C = <double*>malloc(sizeof(double)*N)
target_distances_C = <double **>malloc(sizeof(double*)*nrows)
distance_types_C = <int **>malloc(sizeof(int*)*nrows)
for i in range(N):
z_C[i] = z[i]
for i in range(nrows):
target_distances_C[i] = <double *>malloc(sizeof(double)*ncols)
distance_types_C[i] = <int *>malloc(sizeof(int)*ncols)
for j in range(ncols):
target_distances_C[i][j] = target_distances[i,j]
distance_types_C[i][j] = distance_types[i,j]
stress_val = stress(z_C, target_distances_C,
distance_types_C, step, nrows, ncols, dim)
for i in range(nrows):
free(target_distances_C[i])
free(distance_types_C[i])
free(target_distances_C)
free(distance_types_C)
return stress_val
And the external ctools.c is
#include<stdio.h>
#include<math.h>
double decay(double x){
return 1/(1+exp(-10*x));
}
double** dist_pairs(double* z, int nrows, int ncols, int dims)
{
int i,j,d;
double coord1, coord2;
double** dist;
dist = (double **)malloc(sizeof(double*)*nrows);
for (i=0; i<nrows; i++){
dist[i] = (double *)malloc(sizeof(double)*ncols);
}
for (i=0; i<nrows; i++){
for (j=0; j<ncols; j++){
dist[i][j] = 0;
for (d=0; d<dims; d++){
coord1 = z[d*(nrows+ncols) + i];
coord2 = z[d*(nrows+ncols) + nrows + j];
dist[i][j] += pow(coord1-coord2,2.0) ;
}
dist[i][j] = sqrt(dist[i][j]);
}
}
return dist;
}
double stress(double* z, double **target_distances, int** distance_types,
double step, int nrows, int ncols, int dim){
int i,j;
double stress = 0.0;
double err = 0.0;
double **map_distances = dist_pairs(z, nrows, ncols, dim);
for (i=0; i<nrows; i++){
for (j=0; j<ncols; j++){
if (distance_types[i][j]==0 || distance_types[i][j]==3){
stress += pow(target_distances[i][j] - map_distances[i][j],2.0);
}
else if (distance_types[i][j]==2){
err = target_distances[i][j] - map_distances[i][j] + step;
stress += pow(step,2)*decay(step);
}
}
}
for (i=0; i<nrows; i++){
free(map_distances[i]);
}
free(map_distances);
return stress;
}
I do the following test (test.py)
import numpy as np
import time
from calculus import stress_cython
from pycalculus import stress
nrows = 3000
ncols = 2000
dim = 2
N = 100
is_discrete = 1.0
dt0 = 0
dt1 = 0
difs = []
for i in range(N):
coordinates = np.random.rand(dim, nrows+ncols)
coordinates_flat = coordinates.flatten()
target_distances = np.random.rand(nrows, ncols)
distance_types = np.zeros((nrows,ncols), dtype='i')
t0 = time.time()
stress1 = stress_cython(coordinates_flat, target_distances, distance_types, is_discrete, dim, nrows,
ncols)
t1 = time.time()
stress2 = stress(coordinates.T.flatten(), target_distances, dim, distance_types, is_discrete,
nrows,ncols)
t2 = time.time()
dt0 += t1-t0
dt1 += t2-t1
difs.append(stress1-stress2)
print(f'cython:{dt0:.2f} python:{dt1:.2f}')
and the timings end up pretty similar in my machine (~4.47 vs ~4.62). I am not quite understanding why, is there some parallelised computation going for numpy and scipy in the background?
When I check my core usage, it does not seem to be so. I tried turning of parallelism via
export MKL_NUM_THREADS=1
export NUMEXPR_NUM_THREADS=1
export OMP_NUM_THREADS=1
in the shell before running test.py and it did not make a difference. Is parallelism during vectorized operations is what I am missing here or am I just tring to optimize routines which already have been optimized to best possible extend in these libraries?
ps1: I compile cython code with the arg "-ffast-math"
ps2: In the end I will have all the cores engaged with different initial starting conditions so if numpy automatically parallelises vectorized operations, it wont help me. That is one of the reasons why I am trying to understand what is going on.