5 votes

Pourquoi l'ordre des appels de fonction a-t-il un impact sur l'exécution

J'utilise pyTorch pour exécuter des calculs sur mon GPU (RTX 3000, CUDA 11.1). Une étape consiste à calculer la distance entre un point et un tableau de points. Pour le coup, j'ai testé 2 fonctions pour déterminer laquelle est la plus rapide comme suit :

import datetime as dt
import functools
import timeit
import torch
import numpy as np

device = torch.device("cuda:0")

# define functions for calculating distance
def dist_geom(a, b):
    dist = (a - b)**2
    dist = dist.sum(axis=1)**0.5

    return dist

def dist_linalg(a, b):
    dist = torch.linalg.norm(a - b, axis=1)

    return dist

# create dummy data
a = np.random.randint(0, 100000, (100000, 10, 10)).astype(np.float64)
b = np.random.randint(0, 100000, (1, 10)).astype(np.float64)

# send data to GPU
a = torch.from_numpy(a).to(device)
b = torch.from_numpy(b).to(device)

# test runtime of each
iterations = 1000

t = timeit.Timer(functools.partial(dist_linalg, a, b))
linalg_delta = t.timeit(number=iterations) / iterations
print("Linear algebra time: ", linalg_delta, " seconds per iteration")

t = timeit.Timer(functools.partial(dist_geom, a, b))
geom_delta = t.timeit(number=iterations) / iterations
print("Geometry time: ", geom_delta, " seconds per iteration")

print("linear algebra:geometry ratio: ", linalg_delta / geom_delta)

Cela donne le résultat suivant :

Linear algebra time:  0.000743145  seconds per iteration
Geometry time:  0.001446731  seconds per iteration
linear algebra:geometry ratio:  0.5136718574496572

Donc la fonction d'algèbre linéaire est ~2x plus rapide. Mais si j'appelle la fonction de géométrie en premier :

t = timeit.Timer(functools.partial(dist_geom, a, b))
geom_delta = t.timeit(number=iterations) / iterations
print("Geometry time: ", geom_delta, " seconds per iteration")

t = timeit.Timer(functools.partial(dist_linalg, a, b))
linalg_delta = t.timeit(number=iterations) / iterations
print("Linear algebra time: ", linalg_delta, " seconds per iteration")      

print("linear algebra:geometry ratio: ", linalg_delta / geom_delta)

J'obtiens ce résultat :

Geometry time:  0.001213497  seconds per iteration
Linear algebra time:  0.001136769  seconds per iteration
linear algebra:geometry ratio:  0.9367711663069623

Le temps de dist_geom est presque identique à celui de l'exécution initiale, mais le temps de dist_linalg est maintenant 1,46x plus long !

J'ai testé cela de plusieurs façons et le résultat est toujours le même : l'ordre d'appel semble avoir de l'importance... beaucoup. Je pense qu'il me manque un point fondamental ici, donc toute aide pour comprendre ce qui se passe sera appréciée (et je soupçonne que ce sera si simple que je me sentirai stupide).

J'ai créé deux ensembles de tenseurs. Ce qui suit donne le même temps d'exécution quel que soit l'ordre.

# create 2 tensors for geometry test
a1 = np.random.randint(0, 100000, (100000, 10, 10)).astype(np.float64)
b1 = np.random.randint(0, 100000, (1, 10)).astype(np.float64)

a1 = torch.from_numpy(a1).to(device)
b1 = torch.from_numpy(b1).to(device)

t = timeit.Timer(functools.partial(dist_geom, a, b))
geom_delta = t.timeit(number=iterations) / iterations
print("Geometry time: ", geom_delta, " seconds per iteration")

# create 2 different tensors for the linalg function
a2 = np.random.randint(0, 100000, (100000, 10, 10)).astype(np.float64)
b2 = np.random.randint(0, 100000, (1, 10)).astype(np.float64)

a2 = torch.from_numpy(a2).to(device)
b2 = torch.from_numpy(b2).to(device)

t = timeit.Timer(functools.partial(dist_linalg, a, b))
linalg_delta = t.timeit(number=iterations) / iterations
print("Linear algebra time: ", linalg_delta, " seconds per iteration")      

print("linear algebra:geometry ratio: ", linalg_delta / geom_delta)

Geometry time:  0.0012010019999999998  seconds per iteration
Linear algebra time:  0.0007349769999999999  seconds per iteration
linear algebra:geometry ratio:  0.6119698385181707

Cela dit, si je définis à la fois a1/b1 et a2/b2 avant les appels de fonction, je vois à nouveau la différence de temps. Au départ, je pensais que cela était dû aux temps de chargement de la mémoire, mais cela ne correspond pas vraiment, n'est-ce pas ?

2voto

dimabendera Points 76

Vous pouvez simplement ajouter

torch.cuda.empty_cache()

Tous les codes :

import datetime as dt
import functools
import timeit
import torch
import numpy as np

device = torch.device("cuda:0")

# define functions for calculating distance
def dist_geom(a, b):
    dist = (a - b)**2
    dist = dist.sum(axis=1)**0.5

    return dist

def dist_linalg(a, b):
    dist = torch.linalg.norm(a - b, axis=1)

    return dist

# create dummy data
a = np.random.randint(0, 100000, (100000, 10, 10)).astype(np.float64)
b = np.random.randint(0, 100000, (1, 10)).astype(np.float64)

# send data to GPU
a = torch.from_numpy(a).to(device)
b = torch.from_numpy(b).to(device)

# test runtime of each
iterations = 1000

t = timeit.Timer(functools.partial(dist_linalg, a, b))
linalg_delta = t.timeit(number=iterations) / iterations
print("Linear algebra time: ", linalg_delta, " seconds per iteration")

torch.cuda.empty_cache()

t = timeit.Timer(functools.partial(dist_geom, a, b))
geom_delta = t.timeit(number=iterations) / iterations
print("Geometry time: ", geom_delta, " seconds per iteration")

print("linear algebra:geometry ratio: ", linalg_delta / geom_delta)

0voto

mlucy Points 4747

Le problème est que les deux fonctions que vous comparez sont si rapides que vous finissez par comparer principalement le coût de la gestion de la mémoire de Torch sur le GPU (puisque vous allouez de l'espace pour faire le travail à chaque appel).

Vous pouvez le constater en réduisant le nombre d'exécutions. Si vous réduisez le nombre d'exécutions à 100 au lieu de 1000, timeit mesure beaucoup moins de secondes par itération (environ 10x plus rapide sur ma machine). Je ne sais pas si c'est parce que la gestion de la mémoire de Cuda devient plus coûteuse au fur et à mesure que des allocations se produisent, ou si elle déclenche des clearers plus souvent, ou quoi que ce soit.

De ce fait, quelle que soit la fonction que vous chronométrez en second lieu, elle semblera s'exécuter plus lentement, car cuda a déjà effectué un certain nombre d'opérations de gestion de la mémoire au moment où ce code s'exécute.

La façon de résoudre ce problème dépend de ce que vous voulez mesurer.

Si vous prévoyez d'appeler ces fonctions dans un paradigme où vous ne vous inquiétez pas de la surcharge de la gestion de la mémoire, alors ce que vous voulez faire, c'est remplacer timeit par une boucle manuelle, et à chaque itération de la boucle appeler torch.cuda.empty_cache() avant chronométrer l'appel de la fonction.

Si vous prévoyez d'appeler ces fonctions dans un paradigme où vous sont qui s'inquiète de la gestion de la mémoire, la solution décrite ci-dessus consistant à appeler torch.cuda.empty_cache avant chaque boucle est probablement la meilleure, car elle mesurera avec précision si l'une d'entre elles a une surcharge de gestion de la mémoire plus importante. Dans ce cas, je m'assurerais que le nombre d'exécutions est suffisamment important pour que vous mesuriez le coût de la gestion de la mémoire en régime permanent, car c'est probablement ce qui vous préoccupera plus tard.

Sinon, je ne m'en préoccuperais pas trop ; toute fonction qui s'exécute si rapidement qu'elle est dominée par la gestion de la mémoire ne vaut généralement pas la peine d'être optimisée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X