web-dev-qa-db-fra.com

Python: réécrire une fonction mathématique numpy en boucle à exécuter sur GPU

Quelqu'un peut-il m'aider à réécrire cette fonction (la fonction doTheMath) pour faire les calculs sur le GPU? J'ai utilisé quelques bons jours maintenant pour essayer de m'en sortir mais sans résultat. Je me demande peut-être que quelqu'un peut m'aider à réécrire cette fonction de la manière qui vous semble la plus appropriée car je donne le même résultat à la fin. J'ai essayé d'utiliser @jit De numba mais pour une raison quelconque, il est en réalité beaucoup plus lent que d'exécuter le code comme d'habitude. Avec une énorme taille d'échantillon, l'objectif est de réduire considérablement le temps d'exécution, donc je pense que le GPU est le moyen le plus rapide de le faire.

Je vais expliquer un peu ce qui se passe réellement. Les données réelles, qui semblent presque identiques car les données d'exemple créées dans le code ci-dessous sont divisées en tailles d'échantillons d'environ 5.000.000 de lignes par échantillon ou environ 150 Mo par fichier. Au total, il y a environ 600 000 000 lignes ou 20 Go de données. Je dois parcourir ces données, échantillon par échantillon, puis ligne par ligne dans chaque échantillon, prendre les 2000 dernières lignes (ou une autre) de chaque ligne et exécuter la fonction doTheMath qui renvoie un résultat. Ce résultat est ensuite enregistré sur le disque dur où je peux faire d'autres choses avec un autre programme. Comme vous pouvez le voir ci-dessous, je n'ai pas besoin de tous les résultats de toutes les lignes, seulement ceux plus grands qu'un montant spécifique. Si j'exécute ma fonction telle qu'elle est en ce moment en python j'obtiens environ 62 secondes par 1.000.000 de lignes. Cela prend beaucoup de temps compte tenu de toutes les données et de la rapidité avec laquelle cela doit être fait.

Je dois mentionner que je télécharge le fichier de données réelles par fichier dans le RAM à l'aide de data = joblib.load(file) donc le téléchargement des données n'est pas le problème car cela ne prend que 0,29 seconde environ par fichier. Une fois téléchargé, j'exécute l'intégralité du code ci-dessous. Ce qui prend le plus de temps est la fonction doTheMath. Je suis prêt à donner tous mes 500 points de réputation que j'ai sur stackoverflow en récompense pour quelqu'un qui veut aider je réécris ce code simple pour l'exécuter sur le GPU. Mon intérêt est spécifiquement dans le GPU, je veux vraiment voir comment cela se fait sur ce problème.

EDIT/UPDATE 1: Voici un lien vers un petit échantillon des données réelles: data_csv.Zip Environ 102000 lignes de données réelles1 et 2000 lignes pour les données réelles2a et données2b. Utilisez minimumLimit = 400 Sur les données réelles de l'échantillon

EDIT/UPDATE 2: Pour ceux qui suivent ce post, voici un bref résumé des réponses ci-dessous. Jusqu'à présent, nous avons 4 réponses à la solution originale. Celui proposé par @Divakar ne sont que des ajustements au code d'origine. Des deux réglages, seul le premier est réellement applicable à ce problème, le second est un bon réglage mais ne s'applique pas ici. Sur les trois autres réponses, deux d'entre elles sont des solutions basées sur le processeur et un essai tensorflow-GPU. Le Tensorflow-GPU de Paul Panzer semble prometteur mais quand je le lance sur le GPU, il est plus lent que l'original, donc le code doit encore être amélioré.

Les deux autres solutions basées sur CPU sont soumises par @PaulPanzer (une solution numpy pure) et @MSeifert (une solution numba). Les deux solutions donnent de très bons résultats et les deux données de processus sont extrêmement rapides par rapport au code d'origine. Des deux, celui soumis par Paul Panzer est plus rapide. Il traite environ 1 000 000 lignes en environ 3 secondes. Le seul problème est avec des lots plus petits, cela peut être surmonté en passant à la solution numba proposée par MSeifert, ou même au code d'origine après tous les réglages qui ont été discutés ci-dessous.

Je suis très heureux et reconnaissant à @PaulPanzer et @MSeifert pour le travail qu'ils ont fait sur leurs réponses. Pourtant, puisqu'il s'agit d'une question sur une solution basée sur GPU, j'attends de voir si quelqu'un est prêt à essayer une version GPU et à voir à quelle vitesse les données peuvent être traitées sur le GPU par rapport au CPU actuel solutions. S'il n'y aura pas d'autres réponses surpassant la solution pure numpy de @PaulPanzer, alors j'accepterai sa réponse comme la bonne et j'obtiendrai la prime :)

EDIT/UPDATE 3: @Divakar a publié une nouvelle réponse avec une solution pour le GPU. Après mes tests sur des données réelles, la vitesse n'est même pas comparable aux solutions homologues CPU. Le GPU traite environ 5.000.000 en 1,5 secondes environ. C'est incroyable :) Je suis très enthousiasmé par la solution GPU et je remercie @Divakar de l'avoir publiée. Je remercie aussi @PaulPanzer et @MSeifert pour leurs solutions CPU :) Maintenant, mes recherches se poursuivent à une vitesse incroyable grâce au GPU :)

import pandas as pd
import numpy as np
import time

def doTheMath(tmpData1, data2a, data2b):
    A = tmpData1[:, 0]
    B = tmpData1[:,1]
    C = tmpData1[:,2]
    D = tmpData1[:,3]
    Bmax = B.max()
    Cmin  = C.min()
    dif = (Bmax - Cmin)
    abcd = ((((A - Cmin) / dif) + ((B - Cmin) / dif) + ((C - Cmin) / dif) + ((D - Cmin) / dif)) / 4)
    return np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()

#Declare variables
batchSize = 2000
sampleSize = 5000000
resultArray = []
minimumLimit = 490 #use 400 on the real sample data 

#Create Random Sample Data
data1 = np.matrix(np.random.uniform(1, 100, (sampleSize + batchSize, 4)))
data2a = np.matrix(np.random.uniform(0, 1, (batchSize, 1))) #upper limit
data2b = np.matrix(np.random.uniform(0, 1, (batchSize, 1))) #lower limit
#approx. half of data2a will be smaller than data2b, but that is only in the sample data because it is randomly generated, NOT the real data. The real data2a is always higher than data2b.


#Loop through the data
t0 = time.time()
for rowNr in  range(data1.shape[0]):
    tmp_df = data1[rowNr:rowNr + batchSize] #rolling window
    if(tmp_df.shape[0] == batchSize):
        result = doTheMath(tmp_df, data2a, data2b)
        if (result >= minimumLimit):
            resultArray.append([rowNr , result])
print('Runtime:', time.time() - t0)

#Save data results
resultArray = np.array(resultArray)
print(resultArray[:,1].sum())
resultArray = pd.DataFrame({'index':resultArray[:,0], 'result':resultArray[:,1]})
resultArray.to_csv("Result Array.csv", sep=';')

Les spécifications PC sur lesquelles je travaille:

GTX970(4gb) video card; 
i7-4790K CPU 4.00Ghz; 
16GB RAM;
a SSD drive 
running Windows 7; 

En guise de question secondaire, une deuxième carte vidéo dans SLI aiderait-elle à résoudre ce problème?

20
RaduS

Code d'introduction et de solution

Eh bien, vous l'avez demandé! Donc, répertorié dans ce post est une implémentation avec PyCUDA qui utilise des wrappers légers étendant la plupart des capacités de CUDA dans l'environnement Python. Nous allons utiliser sa fonctionnalité SourceModule qui nous permet d'écrire et de compiler les noyaux CUDA en restant dans l'environnement Python.

Pour en venir au problème, parmi les calculs impliqués, nous avons un maximum et un minimum glissants, quelques différences et divisions et comparaisons. Pour les parties maximum et minimum, qui impliquent la recherche de bloc max (pour chaque fenêtre coulissante), nous utiliserons la technique de réduction comme discuté en détail here . Cela se ferait au niveau du bloc. Pour les itérations de niveau supérieur à travers les fenêtres coulissantes, nous utiliserions l'indexation au niveau de la grille dans les ressources CUDA. Pour plus d'informations sur ce format de bloc et de grille, reportez-vous à page-18 . PyCUDA prend également en charge les fonctions intégrées pour calculer des réductions telles que max et min, mais nous perdons le contrôle, en particulier, nous avons l'intention d'utiliser une mémoire spécialisée comme la mémoire partagée et constante pour tirer parti du GPU à son niveau presque optimal.

Liste du code de la solution PyCUDA-NumPy -

1] Partie PyCUDA -

import pycuda.autoinit
import pycuda.driver as drv
import numpy as np
from pycuda.compiler import SourceModule

mod = SourceModule("""
#define TBP 1024 // THREADS_PER_BLOCK

__device__ void get_Bmax_Cmin(float* out, float *d1, float *d2, int L, int offset)
{
    int tid = threadIdx.x;
    int inv = TBP;
    __shared__ float dS[TBP][2];

    dS[tid][0] = d1[tid+offset];  
    dS[tid][1] = d2[tid+offset];         
    __syncthreads();

    if(tid<L-TBP)  
    {
        dS[tid][0] = fmaxf(dS[tid][0] , d1[tid+inv+offset]);
        dS[tid][1] = fminf(dS[tid][1] , d2[tid+inv+offset]);
    }
    __syncthreads();
    inv = inv/2;

    while(inv!=0)   
    {
        if(tid<inv)
        {
            dS[tid][0] = fmaxf(dS[tid][0] , dS[tid+inv][0]);
            dS[tid][1] = fminf(dS[tid][1] , dS[tid+inv][1]);
        }
        __syncthreads();
        inv = inv/2;
    }
    __syncthreads();

    if(tid==0)
    {
        out[0] = dS[0][0];
        out[1] = dS[0][1];
    }   
    __syncthreads();
}

__global__ void main1(float* out, float *d0, float *d1, float *d2, float *d3, float *lowL, float *highL, int *BLOCKLEN)
{
    int L = BLOCKLEN[0];
    int tid = threadIdx.x;
    int iterID = blockIdx.x;
    float Bmax_Cmin[2];
    int inv;
    float Cmin, dif;   
    __shared__ float dS[TBP*2];   

    get_Bmax_Cmin(Bmax_Cmin, d1, d2, L, iterID);  
    Cmin = Bmax_Cmin[1];
    dif = (Bmax_Cmin[0] - Cmin);

    inv = TBP;

    dS[tid] = (d0[tid+iterID] + d1[tid+iterID] + d2[tid+iterID] + d3[tid+iterID] - 4.0*Cmin) / (4.0*dif);
    __syncthreads();

    if(tid<L-TBP)  
        dS[tid+inv] = (d0[tid+inv+iterID] + d1[tid+inv+iterID] + d2[tid+inv+iterID] + d3[tid+inv+iterID] - 4.0*Cmin) / (4.0*dif);                   

     dS[tid] = ((dS[tid] >= lowL[tid]) & (dS[tid] <= highL[tid])) ? 1 : 0;
     __syncthreads();

     if(tid<L-TBP)
         dS[tid] += ((dS[tid+inv] >= lowL[tid+inv]) & (dS[tid+inv] <= highL[tid+inv])) ? 1 : 0;
     __syncthreads();

    inv = inv/2;
    while(inv!=0)   
    {
        if(tid<inv)
            dS[tid] += dS[tid+inv];
        __syncthreads();
        inv = inv/2;
    }

    if(tid==0)
        out[iterID] = dS[0];
    __syncthreads();

}
""")

Veuillez noter que THREADS_PER_BLOCK, TBP doit être défini sur la base de batchSize. La règle générale ici est d'attribuer une puissance de 2 à TBP qui est juste inférieure à batchSize. Ainsi, pour batchSize = 2000, nous avions besoin de TBP comme 1024.

2] Partie NumPy -

def gpu_app_v1(A, B, C, D, batchSize, minimumLimit):
    func1 = mod.get_function("main1")
    outlen = len(A)-batchSize+1

    # Set block and grid sizes
    BSZ = (1024,1,1)
    GSZ = (outlen,1)

    dest = np.zeros(outlen).astype(np.float32)
    N = np.int32(batchSize)
    func1(drv.Out(dest), drv.In(A), drv.In(B), drv.In(C), drv.In(D), \
                     drv.In(data2b), drv.In(data2a),\
                     drv.In(N), block=BSZ, grid=GSZ)
    idx = np.flatnonzero(dest >= minimumLimit)
    return idx, dest[idx]

Analyse comparative

J'ai testé sur GTX 960M. Veuillez noter que PyCUDA s'attend à ce que les tableaux soient d'un ordre contigu. Donc, nous devons couper les colonnes et faire des copies. J'attends/je suppose que les données pourraient être lues à partir des fichiers de telle sorte que les données soient réparties sur des lignes au lieu d'être sous forme de colonnes. Ainsi, garder ceux-ci hors de la fonction d'analyse comparative pour l'instant.

Approche originale -

def org_app(data1, batchSize, minimumLimit):
    resultArray = []
    for rowNr in  range(data1.shape[0]-batchSize+1):
        tmp_df = data1[rowNr:rowNr + batchSize] #rolling window
        result = doTheMath(tmp_df, data2a, data2b)
        if (result >= minimumLimit):
            resultArray.append([rowNr , result]) 
    return resultArray

Délais et vérification -

In [2]: #Declare variables
   ...: batchSize = 2000
   ...: sampleSize = 50000
   ...: resultArray = []
   ...: minimumLimit = 490 #use 400 on the real sample data
   ...: 
   ...: #Create Random Sample Data
   ...: data1 = np.random.uniform(1, 100000, (sampleSize + batchSize, 4)).astype(np.float32)
   ...: data2b = np.random.uniform(0, 1, (batchSize)).astype(np.float32)
   ...: data2a = data2b + np.random.uniform(0, 1, (batchSize)).astype(np.float32)
   ...: 
   ...: # Make column copies
   ...: A = data1[:,0].copy()
   ...: B = data1[:,1].copy()
   ...: C = data1[:,2].copy()
   ...: D = data1[:,3].copy()
   ...: 
   ...: gpu_out1,gpu_out2 = gpu_app_v1(A, B, C, D, batchSize, minimumLimit)
   ...: cpu_out1,cpu_out2 = np.array(org_app(data1, batchSize, minimumLimit)).T
   ...: print(np.allclose(gpu_out1, cpu_out1))
   ...: print(np.allclose(gpu_out2, cpu_out2))
   ...: 
True
False

Il y a donc des différences entre les comptages CPU et GPU. Étudions-les -

In [7]: idx = np.flatnonzero(~np.isclose(gpu_out2, cpu_out2))

In [8]: idx
Out[8]: array([12776, 15208, 17620, 18326])

In [9]: gpu_out2[idx] - cpu_out2[idx]
Out[9]: array([-1., -1.,  1.,  1.])

Il existe quatre cas de dénombrements non correspondants. Ceux-ci sont désactivés au maximum par 1. Après des recherches, je suis tombé sur des informations à ce sujet. Fondamentalement, puisque nous utilisons des mathématiques intrinsèques pour les calculs max et min et que je pense que le dernier bit binaire de la représentation flottante pt est différent de celui du processeur. Ceci est appelé erreur ULP et a été discuté en détail here et here .

Enfin, mis de côté le problème, passons à l'élément le plus important, la performance -

In [10]: %timeit org_app(data1, batchSize, minimumLimit)
1 loops, best of 3: 2.18 s per loop

In [11]: %timeit gpu_app_v1(A, B, C, D, batchSize, minimumLimit)
10 loops, best of 3: 82.5 ms per loop

In [12]: 2180.0/82.5
Out[12]: 26.424242424242426

Essayons avec de plus grands ensembles de données. Avec sampleSize = 500000, on a -

In [14]: %timeit org_app(data1, batchSize, minimumLimit)
1 loops, best of 3: 23.2 s per loop

In [15]: %timeit gpu_app_v1(A, B, C, D, batchSize, minimumLimit)
1 loops, best of 3: 821 ms per loop

In [16]: 23200.0/821
Out[16]: 28.25822168087698

Ainsi, l'accélération reste constante à environ 27.

Limitations:

1) Nous utilisons float32 nombres, car les GPU fonctionnent mieux avec ceux-ci. La double précision spécialement sur les GPU non-serveur n'est pas populaire en termes de performances et puisque vous travaillez avec un tel GPU, j'ai testé avec float32.

Nouvelle amélioration:

1) Nous pourrions utiliser plus rapidement constant memory pour alimenter data2a et data2b, plutôt que d'utiliser global memory.

8
Divakar

Tweak # 1

Il est généralement conseillé de vectoriser les choses lorsque vous travaillez avec des tableaux NumPy. Mais avec de très grands tableaux, je pense que vous n'avez plus d'options là-bas. Ainsi, pour améliorer les performances, un Tweak mineur est possible d'optimiser la dernière étape de sommation.

Nous pourrions remplacer l'étape qui fait un tableau de 1s et 0s et résume:

np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()

avec np.count_nonzero qui fonctionne efficacement pour compter les valeurs de True dans un tableau booléen, au lieu de les convertir en 1s et 0s -

np.count_nonzero((abcd <= data2a) & (abcd >= data2b))

Test d'exécution -

In [45]: abcd = np.random.randint(11,99,(10000))

In [46]: data2a = np.random.randint(11,99,(10000))

In [47]: data2b = np.random.randint(11,99,(10000))

In [48]: %timeit np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()
10000 loops, best of 3: 81.8 µs per loop

In [49]: %timeit np.count_nonzero((abcd <= data2a) & (abcd >= data2b))
10000 loops, best of 3: 28.8 µs per loop

Tweak # 2

Utilisez une réciproque pré-calculée lorsque vous traitez des cas qui subissent une diffusion implicite. Quelques informations supplémentaires here . Ainsi, stockez l'inverse de dif et utilisez-le à l'étape:

((((A  - Cmin) / dif) + ((B  - Cmin) / dif) + ...

Échantillon test -

In [52]: A = np.random.Rand(10000)

In [53]: dif = 0.5

In [54]: %timeit A/dif
10000 loops, best of 3: 25.8 µs per loop

In [55]: %timeit A*(1.0/dif)
100000 loops, best of 3: 7.94 µs per loop

Vous disposez de quatre emplacements utilisant la division par dif. Donc, j'espère que cela apporterait un coup de pouce notable là aussi!

10
Divakar

Avant de commencer à modifier la cible (GPU) ou à utiliser autre chose (c'est-à-dire des exécutions parallèles), vous voudrez peut-être réfléchir à la manière d'améliorer le code déjà existant. Vous avez utilisé la balise numba - donc je vais l'utiliser pour améliorer le code: d'abord nous opérons sur des tableaux et non sur des matrices:

data1 = np.array(np.random.uniform(1, 100, (sampleSize + batchSize, 4)))
data2a = np.array(np.random.uniform(0, 1, batchSize)) #upper limit
data2b = np.array(np.random.uniform(0, 1, batchSize)) #lower limit

Chaque fois que vous appelez doTheMath, vous vous attendez à un entier, mais vous utilisez beaucoup de tableaux et créez un grand nombre de tableaux intermédiaires:

abcd = ((((A  - Cmin) / dif) + ((B  - Cmin) / dif) + ((C   - Cmin) / dif) + ((D - Cmin) / dif)) / 4)
return np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()

Cela crée un tableau intermédiaire à chaque étape:

  • tmp1 = A-Cmin,
  • tmp2 = tmp1 / dif,
  • tmp3 = B - Cmin,
  • tmp4 = tmp3 / dif
  • ... vous obtenez l'essentiel.

Cependant, il s'agit d'une fonction de réduction (tableau -> entier), donc avoir beaucoup de tableaux intermédiaires est un poids inutile, il suffit de calculer la valeur de la "mouche".

import numba as nb

@nb.njit
def doTheMathNumba(tmpData, data2a, data2b):
    Bmax = np.max(tmpData[:, 1])
    Cmin = np.min(tmpData[:, 2])
    diff = (Bmax - Cmin)
    idiff = 1 / diff
    sum_ = 0
    for i in range(tmpData.shape[0]):
        val = (tmpData[i, 0] + tmpData[i, 1] + tmpData[i, 2] + tmpData[i, 3]) / 4 * idiff - Cmin * idiff
        if val <= data2a[i] and val >= data2b[i]:
            sum_ += 1
    return sum_

J'ai fait autre chose ici pour éviter plusieurs opérations:

(((A - Cmin) / dif) + ((B - Cmin) / dif) + ((C - Cmin) / dif) + ((D - Cmin) / dif)) / 4
= ((A - Cmin + B - Cmin + C - Cmin + D - Cmin) / dif) / 4
= (A + B + C + D - 4 * Cmin) / (4 * dif)
= (A + B + C + D) / (4 * dif) - (Cmin / dif)

Cela réduit en fait le temps d'exécution de près d'un facteur 10 sur mon ordinateur:

%timeit doTheMath(tmp_df, data2a, data2b)       # 1000 loops, best of 3: 446 µs per loop
%timeit doTheMathNumba(tmp_df, data2a, data2b)  # 10000 loops, best of 3: 59 µs per loop

Il y a certainement aussi d'autres améliorations, par exemple en utilisant un roulement min/max pour calculer Bmax et Cmin, qui feraient au moins une partie du calcul exécuté dans O(sampleSize) à la place de O(samplesize * batchsize). Cela permettrait également de réutiliser certains des calculs (A + B + C + D) / (4 * dif) - (Cmin / dif) Car si Cmin et Bmax ne changent pas pour l'échantillon suivant, ces valeurs ne diffèrent pas. C'est un peu compliqué à faire car les comparaisons diffèrent. Mais certainement possible! Vois ici:

import time
import numpy as np
import numba as nb

@nb.njit
def doTheMathNumba(abcd, data2a, data2b, Bmax, Cmin):
    diff = (Bmax - Cmin)
    idiff = 1 / diff
    quarter_idiff = 0.25 * idiff
    sum_ = 0
    for i in range(abcd.shape[0]):
        val = abcd[i] * quarter_idiff - Cmin * idiff
        if val <= data2a[i] and val >= data2b[i]:
            sum_ += 1
    return sum_

@nb.njit
def doloop(data1, data2a, data2b, abcd, Bmaxs, Cmins, batchSize, sampleSize, minimumLimit, resultArray):
    found = 0
    for rowNr in range(data1.shape[0]):
        if(abcd[rowNr:rowNr + batchSize].shape[0] == batchSize):
            result = doTheMathNumba(abcd[rowNr:rowNr + batchSize], 
                                    data2a, data2b, Bmaxs[rowNr], Cmins[rowNr])
            if (result >= minimumLimit):
                resultArray[found, 0] = rowNr
                resultArray[found, 1] = result
                found += 1
    return resultArray[:found]

#Declare variables
batchSize = 2000
sampleSize = 50000
resultArray = []
minimumLimit = 490 #use 400 on the real sample data 

data1 = np.array(np.random.uniform(1, 100, (sampleSize + batchSize, 4)))
data2a = np.array(np.random.uniform(0, 1, batchSize)) #upper limit
data2b = np.array(np.random.uniform(0, 1, batchSize)) #lower limit

from scipy import ndimage
t0 = time.time()
abcd = np.sum(data1, axis=1)
Bmaxs = ndimage.maximum_filter1d(data1[:, 1], 
                                 size=batchSize, 
                                 Origin=-((batchSize-1)//2-1))  # correction for even shapes
Cmins = ndimage.minimum_filter1d(data1[:, 2], 
                                 size=batchSize, 
                                 Origin=-((batchSize-1)//2-1))

result = np.zeros((sampleSize, 2), dtype=np.int64)
doloop(data1, data2a, data2b, abcd, Bmaxs, Cmins, batchSize, sampleSize, minimumLimit, result)
print('Runtime:', time.time() - t0)

Cela me donne un Runtime: 0.759593152999878 (Après que numba a compilé les fonctions!), Alors que votre original a pris Runtime: 24.68975639343262. Maintenant, nous sommes 30 fois plus rapides!

Avec votre taille d'échantillon, il faut toujours Runtime: 60.187848806381226 Mais ce n'est pas trop mal, non?

Et même si je ne l'ai pas fait moi-même, numba dit qu'il est possible d'écrire "Numba pour les GPU CUDA" et cela ne semble pas compliqué.

5
MSeifert

Voici du code pour démontrer ce qui est possible en ajustant simplement l'algorithme. C'est purement numpy mais sur les exemples de données que vous avez publiés donne une accélération d'environ 35x par rapport à la version originale (~ 1 000 000 d'échantillons en ~ 2,5 secondes sur ma machine plutôt modeste):

>>> result_dict = master('run')
[('load', 0.82578349113464355), ('precomp', 0.028138399124145508), ('max/min', 0.24333405494689941), ('ABCD', 0.015314102172851562), ('main', 1.3356468677520752)]
TOTAL 2.44821691513

Tweaks utilisés:

  • A + B + C + D, voir mon autre réponse
  • exécutant min/max, y compris en évitant de calculer (A + B + C + D - 4Cmin)/(4dif) plusieurs fois avec le même Cmin/dif.

Ce sont plus ou moins routiniers. Cela laisse la comparaison avec data2a/b qui est cher O(NK) où N est le nombre d'échantillons et K est la taille de la fenêtre. Ici, on peut profiter du relativement bien- En utilisant le min/max en cours d'exécution, on peut créer des variantes de data2a/b qui peuvent être utilisées pour tester une plage de décalages de fenêtre à la fois, si le test échoue, tous ces décalages peuvent être exclus immédiatement, sinon la plage est bissectée .

import numpy as np
import time

# global variables; they will hold the precomputed pre-screening filters
preA, preB = {}, {}
CHUNK_SIZES = None

def sliding_argmax(data, K=2000):
    """compute the argmax of data over a sliding window of width K

    returns:
        indices  -- indices into data
        switches -- window offsets at which the maximum changes
                    (strictly speaking: where the index of the maximum changes)
                    excludes 0 but includes maximum offset (len(data)-K+1)

    see last line of compute_pre_screening_filter for a recipe to convert
    this representation to the vector of maxima
    """
    N = len(data)
    last = np.argmax(data[:K])
    indices = [last]
    while indices[-1] <= N - 1:
        ge = np.where(data[last + 1 : last + K + 1] > data[last])[0]
        if len(ge) == 0:
            if last + K >= N:
                break
            last += 1 + np.argmax(data[last + 1 : last + K + 1])
            indices.append(last)
        else:
            last += 1 + ge[0]
            indices.append(last)
    indices = np.array(indices)
    switches = np.where(data[indices[1:]] > data[indices[:-1]],
                        indices[1:] + (1-K), indices[:-1] + 1)
    return indices, np.r_[switches, [len(data)-K+1]]


def compute_pre_screening_filter(bound, n_offs):
    """compute pre-screening filter for point-wise upper bound

    given a K-vector of upper bounds B and K+n_offs-1-vector data
    compute K+n_offs-1-vector filter such that for each index j
    if for any offset 0 <= o < n_offs and index 0 <= i < K such that
    o + i = j, the inequality B_i >= data_j holds then filter_j >= data_j

    therefore the number of data points below filter is an upper bound for
    the maximum number of points below bound in any K-window in data
    """
    pad_l, pad_r = np.min(bound[:n_offs-1]), np.min(bound[1-n_offs:])
    padded = np.r_[pad_l+np.zeros(n_offs-1,), bound, pad_r+np.zeros(n_offs-1,)]
    indices, switches = sliding_argmax(padded, n_offs)
    return padded[indices].repeat(np.diff(np.r_[[0], switches]))


def compute_all_pre_screening_filters(upper, lower, min_chnk=5, dyads=6):
    """compute upper and lower pre-screening filters for data blocks of
    sizes K+n_offs-1 where
    n_offs = min_chnk, 2min_chnk, ..., 2^(dyads-1)min_chnk

    the result is stored in global variables preA and preB
    """
    global CHUNK_SIZES

    CHUNK_SIZES = min_chnk * 2**np.arange(dyads)
    preA[1] = upper
    preB[1] = lower
    for n in CHUNK_SIZES:
        preA[n] = compute_pre_screening_filter(upper, n)
        preB[n] = -compute_pre_screening_filter(-lower, n)


def test_bounds(block, counts, threshold=400):
    """test whether the windows fitting in the data block 'block' fall
    within the bounds using pre-screening for efficient bulk rejection

    array 'counts' will be overwritten with the counts of compliant samples
    note that accurate counts will only be returned for above threshold
    windows, because the analysis of bulk rejected windows is short-circuited

    also note that bulk rejection only works for 'well behaved' data and
    for example not on random numbers
    """
    N = len(counts)
    K = len(preA[1])
    r = N % CHUNK_SIZES[0]
    # chop up N into as large as possible chunks with matching pre computed
    # filters
    # start with small and work upwards
    counts[:r] = [np.count_nonzero((block[l:l+K] <= preA[1]) &
                                   (block[l:l+K] >= preB[1]))
                  for l in range(r)]

    def bisect(block, counts):
        M = len(counts)
        cnts = np.count_nonzero((block <= preA[M]) & (block >= preB[M]))
        if cnts < threshold:
            counts[:] = cnts
            return
        Elif M == CHUNK_SIZES[0]:
            counts[:] = [np.count_nonzero((block[l:l+K] <= preA[1]) &
                                          (block[l:l+K] >= preB[1]))
                         for l in range(M)]
        else:
            M //= 2
            bisect(block[:-M], counts[:M])
            bisect(block[M:], counts[M:])

    N = N // CHUNK_SIZES[0]
    for M in CHUNK_SIZES:
        if N % 2:
            bisect(block[r:r+M+K-1], counts[r:r+M])
            r += M
        Elif N == 0:
            return
        N //= 2
    else:
        for j in range(2*N):
            bisect(block[r:r+M+K-1], counts[r:r+M])
            r += M


def analyse(data, use_pre_screening=True, min_chnk=5, dyads=6,
            threshold=400):
    samples, upper, lower = data
    N, K = samples.shape[0], upper.shape[0]
    times = [time.time()]
    if use_pre_screening:
        compute_all_pre_screening_filters(upper, lower, min_chnk, dyads)
    times.append(time.time())
    # compute switching points of max and min for running normalisation
    upper_inds, upper_swp = sliding_argmax(samples[:, 1], K)
    lower_inds, lower_swp = sliding_argmax(-samples[:, 2], K)
    times.append(time.time())
    # sum columns
    ABCD = samples.sum(axis=-1)
    times.append(time.time())
    counts = np.empty((N-K+1,), dtype=int)
    # main loop
    # loop variables:
    offs = 0
    u_ind, u_scale, u_swp = 0, samples[upper_inds[0], 1], upper_swp[0]
    l_ind, l_scale, l_swp = 0, samples[lower_inds[0], 2], lower_swp[0]
    while True:
        # check which is switching next, min(C) or max(B)
        if u_swp > l_swp:
            # greedily take the largest block possible such that dif and Cmin
            # do not change
            block = (ABCD[offs:l_swp+K-1] - 4*l_scale) \
                    * (0.25 / (u_scale-l_scale))
            if use_pre_screening:
                test_bounds(block, counts[offs:l_swp], threshold=threshold)
            else:
                counts[offs:l_swp] = [
                    np.count_nonzero((block[l:l+K] <= upper) &
                                     (block[l:l+K] >= lower))
                    for l in range(l_swp - offs)]
            # book keeping
            l_ind += 1
            offs = l_swp
            l_swp = lower_swp[l_ind]
            l_scale = samples[lower_inds[l_ind], 2]
        else:
            block = (ABCD[offs:u_swp+K-1] - 4*l_scale) \
                    * (0.25 / (u_scale-l_scale))
            if use_pre_screening:
                test_bounds(block, counts[offs:u_swp], threshold=threshold)
            else:
                counts[offs:u_swp] = [
                    np.count_nonzero((block[l:l+K] <= upper) &
                                     (block[l:l+K] >= lower))
                    for l in range(u_swp - offs)]
            u_ind += 1
            if u_ind == len(upper_inds):
                assert u_swp == N-K+1
                break
            offs = u_swp
            u_swp = upper_swp[u_ind]
            u_scale = samples[upper_inds[u_ind], 1]
    times.append(time.time())
    return {'counts': counts, 'valid': np.where(counts >= 400)[0],
            'timings': np.diff(times)}


def master(mode='calibrate', data='fake', use_pre_screening=True, nrep=3,
           min_chnk=None, dyads=None):
    t = time.time()
    if data in ('fake', 'load'):
        data1 = np.loadtxt('data1.csv', delimiter=';', skiprows=1,
                           usecols=[1,2,3,4])
        data2a = np.loadtxt('data2a.csv', delimiter=';', skiprows=1,
                            usecols=[1])
        data2b = np.loadtxt('data2b.csv', delimiter=';', skiprows=1,
                            usecols=[1])
        if data == 'fake':
            data1 = np.tile(data1, (10, 1))
        threshold = 400
    Elif data == 'random':
        data1 = np.random.random((102000, 4))
        data2b = np.random.random(2000)
        data2a = np.random.random(2000)
        threshold = 490
        if use_pre_screening or mode == 'calibrate':
            print('WARNING: pre-screening not efficient on artificial data')
    else:
        raise ValueError("data mode {} not recognised".format(data))
    data = data1, data2a, data2b
    t_load = time.time() - t
    if mode == 'calibrate':
        min_chnk = (2, 3, 4, 5, 6) if min_chnk is None else min_chnk
        dyads = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) if dyads is None else dyads
        timings = np.zeros((len(min_chnk), len(dyads)))
        print('max bisect  ' + ' '.join([
            '   n.a.' if dy == 0 else '{:7d}'.format(dy) for dy in dyads]),
              end='')
        for i, mc in enumerate(min_chnk):
            print('\nmin chunk {}'.format(mc), end=' ')
            for j, dy in enumerate(dyads):
                for k in range(nrep):
                    if dy == 0: # no pre-screening
                        timings[i, j] += analyse(
                            data, False, mc, dy, threshold)['timings'][3]
                    else:
                        timings[i, j] += analyse(
                            data, True, mc, dy, threshold)['timings'][3]
                timings[i, j] /= nrep
                print('{:7.3f}'.format(timings[i, j]), end=' ', flush=True)
        best_mc, best_dy = np.unravel_index(np.argmin(timings.ravel()),
                                            timings.shape)
        print('\nbest', min_chnk[best_mc], dyads[best_dy])
        return timings, min_chnk[best_mc], dyads[best_dy]
    if mode == 'run':
        min_chnk = 2 if min_chnk is None else min_chnk
        dyads = 5 if dyads is None else dyads
        res = analyse(data, use_pre_screening, min_chnk, dyads, threshold)
        times = np.r_[[t_load], res['timings']]
        print(list(Zip(('load', 'precomp', 'max/min', 'ABCD', 'main'), times)))
        print('TOTAL', times.sum())
        return res
4
Paul Panzer

C'est techniquement hors sujet (pas GPU) mais je suis sûr que cela vous intéressera.

Il y a une économie évidente et assez importante:

Précalculez A + B + C + D (Pas dans la boucle, sur l'ensemble des données: data1.sum(axis=-1)), car abcd = ((A+B+C+D) - 4Cmin) / (4dif). Cela permettra d'économiser pas mal d'opérations.

Surpris personne n'a repéré celui-là auparavant ;-)

Éditer:

Il y a une autre chose, bien que je soupçonne que ce n'est que dans votre exemple, pas dans vos données réelles:

En l'état, environ la moitié de data2a Sera plus petit que data2b. Dans ces endroits, vos conditions sur abcd ne peuvent pas être toutes les deux vraies, vous n'avez donc même pas besoin de calculer abcd.

Éditer:

Un autre réglage que j'ai utilisé ci-dessous, mais j'ai oublié de mentionner: si vous calculez le maximum (ou le minimum) sur une fenêtre en mouvement. Lorsque vous déplacez un point vers la droite, par exemple, quelle est la probabilité que le maximum change? Il n'y a que deux choses qui peuvent le changer: le nouveau point à droite est plus grand (arrive à peu près une fois dans la longueur de la fenêtre, et même si cela arrive, vous connaissez immédiatement le nouveau max) ou l'ancien max tombe de la fenêtre à gauche (se produit également à peu près une fois par fenêtre). Ce n'est que dans ce dernier cas que vous devez rechercher dans toute la fenêtre le nouveau max.

Éditer:

Impossible de résister à un essai dans tensorflow. Je n'ai pas de GPU, vous devez donc tester la vitesse. Mettez "gpu" pour "cpu" sur la ligne marquée.

Sur le processeur, c'est environ la moitié de la vitesse de votre implémentation d'origine (c'est-à-dire sans les réglages de Divakar). Notez que j'ai pris la liberté de changer les entrées de matrice en tableau simple. Actuellement tensorflow est un peu une cible mobile, alors assurez-vous d'avoir la bonne version. J'ai utilisé Python3.6 et tf 0.12.1 Si vous faites une installation pip3 tensorflow-gpu aujourd'hui, devrait pourrait fonctionner.

import numpy as np
import time
import tensorflow as tf

# currently the max/min code is sequential
# thus
parallel_iterations = 1
# but you can put this in a separate loop, precompute and then try and run
# the remainder of doTheMathTF with a larger parallel_iterations

# tensorflow is quite capricious about its data types
ddf = tf.float64
ddi = tf.int32

def worker(data1, data2a, data2b):
    ###################################
    # CHANGE cpu to gpu in next line! #
    ###################################
    with tf.device('/cpu:0'):
        g = tf.Graph ()
        with g.as_default():
            ABCD = tf.constant(data1.sum(axis=-1), dtype=ddf)
            B = tf.constant(data1[:, 1], dtype=ddf)
            C = tf.constant(data1[:, 2], dtype=ddf)
            window = tf.constant(len(data2a))
            N = tf.constant(data1.shape[0] - len(data2a) + 1, dtype=ddi)
            data2a = tf.constant(data2a, dtype=ddf)
            data2b = tf.constant(data2b, dtype=ddf)
            def doTheMathTF(i, Bmax, Bmaxind, Cmin, Cminind, out):
                # most of the time we can keep the old max/min
                Bmaxind = tf.cond(Bmaxind<i,
                                  lambda: i + tf.to_int32(
                                      tf.argmax(B[i:i+window], axis=0)),
                                  lambda: tf.cond(Bmax>B[i+window-1], 
                                                  lambda: Bmaxind, 
                                                  lambda: i+window-1))
                Cminind = tf.cond(Cminind<i,
                                  lambda: i + tf.to_int32(
                                      tf.argmin(C[i:i+window], axis=0)),
                                  lambda: tf.cond(Cmin<C[i+window-1],
                                                  lambda: Cminind,
                                                  lambda: i+window-1))
                Bmax = B[Bmaxind]
                Cmin = C[Cminind]
                abcd = (ABCD[i:i+window] - 4 * Cmin) * (1 / (4 * (Bmax-Cmin)))
                out = out.write(i, tf.to_int32(
                    tf.count_nonzero(tf.logical_and(abcd <= data2a,
                                                    abcd >= data2b))))
                return i + 1, Bmax, Bmaxind, Cmin, Cminind, out
            with tf.Session(graph=g) as sess:
                i, Bmaxind, Bmax, Cminind, Cmin, out = tf.while_loop(
                    lambda i, _1, _2, _3, _4, _5: i<N, doTheMathTF,
                    (tf.Variable(0, dtype=ddi), tf.Variable(0.0, dtype=ddf),
                     tf.Variable(-1, dtype=ddi),
                     tf.Variable(0.0, dtype=ddf), tf.Variable(-1, dtype=ddi),
                     tf.TensorArray(ddi, size=N)),
                    shape_invariants=None,
                    parallel_iterations=parallel_iterations,
                    back_prop=False)
                out = out.pack()
                sess.run(tf.initialize_all_variables())
                out, = sess.run((out,))
    return out

#Declare variables
batchSize = 2000
sampleSize = 50000#00
resultArray = []

#Create Sample Data
data1 = np.random.uniform(1, 100, (sampleSize + batchSize, 4))
data2a = np.random.uniform(0, 1, (batchSize,))
data2b = np.random.uniform(0, 1, (batchSize,))

t0 = time.time()
out = worker(data1, data2a, data2b)
print('Runtime (tensorflow):', time.time() - t0)


good_indices, = np.where(out >= 490)
res_tf = np.c_[good_indices, out[good_indices]]

def doTheMath(tmpData1, data2a, data2b):
    A = tmpData1[:, 0]
    B  = tmpData1[:,1]
    C   = tmpData1[:,2]
    D = tmpData1[:,3]
    Bmax = B.max()
    Cmin  = C.min()
    dif = (Bmax - Cmin)
    abcd = ((((A  - Cmin) / dif) + ((B  - Cmin) / dif) + ((C   - Cmin) / dif) + ((D - Cmin) / dif)) / 4)
    return np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()

#Loop through the data
t0 = time.time()
for rowNr in  range(sampleSize+1):
    tmp_df = data1[rowNr:rowNr + batchSize] #rolling window
    result = doTheMath(tmp_df, data2a, data2b)
    if (result >= 490):
        resultArray.append([rowNr , result])
print('Runtime (original):', time.time() - t0)
print(np.alltrue(np.array(resultArray)==res_tf))
3
Paul Panzer