File size: 9,688 Bytes
ff388c9
 
 
 
173c265
ff388c9
 
 
 
 
 
 
 
dea020f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
"""
Business Source License 1.1

Copyright (C) 2025 Raman Marozau, raman@worktif.com
Use of this software is governed by the Business Source License included in the LICENSE file and at www.mariadb.com/bsl11.

Change Date: Never
On the date above, in accordance with the Business Source License, use of this software will be governed by the open source license specified in the LICENSE file.
Additional Use Grant: Free for personal and non-commercial research use only.

SPDX-License-Identifier: BUSL-1.1
"""

import gc
import math
import os
import random
from typing import List

import numpy as np
import pytest
import torch
from numba import njit

from purem import purem

purem.configure(license_key="purem-sandbox")

ARRAY_SIZE = int(os.environ.get("ARRAY_SIZE", 200_000))


def generate_numbers(size, start=0.0, end=1.0) -> List[float]:
    """
    Generates a list of random floating-point numbers within a specified range.

    This function creates a list of random numbers based on the size specified by
    the user. The numbers are uniformly distributed and fall within the range
    defined by the `start` and `end` parameters. If `start` and `end` are not provided,
    they default to 0.0 and 1.0, respectively.

    :param size: The number of random numbers to generate.
    :type size: int
    :param start: The lower bound of the range for the random numbers. Defaults to 0.0.
    :type start: float
    :param end: The upper bound of the range for the random numbers. Defaults to 1.0.
    :type end: float
    :return: A list of random floating-point numbers within the specified range.
    :rtype: list[float]
    """
    return [random.uniform(start, end) for _ in range(size)]


@pytest.fixture(autouse=True)
def clean_memory() -> None:
    """
    A pytest fixture to clean up memory before each test.

    This fixture is automatically used for every test function due to the
    `autouse=True` parameter. It ensures that garbage collection is triggered,
    helping maintain a clean state in memory between test executions.

    :return: None
    """
    gc.collect()


@pytest.fixture
def array_instance() -> np.ndarray:
    """
    Creates and returns a numpy array fixture for testing.

    This fixture generates a numpy array based on the provided
    `generate_numbers` function and the defined `ARRAY_SIZE`.
    The array elements are cast to the `float` data type.

    :yield: A numpy array of floating-point numbers for testing.
    :rtype: np.ndarray
    """
    return np.array(generate_numbers(ARRAY_SIZE), dtype=float)


def softmax_purem(arr: np.ndarray) -> np.ndarray:
    """
    Apply the softmax function to the input array.

    The softmax function computes the probabilities for each element
    in the input array by exponentiating them and normalizing the
    results so that they sum to 1. It is commonly used in machine
    learning classification problems.

    :param arr: Input array for which to compute the softmax.
                Should be a NumPy ndarray.
    :type arr: np.ndarray
    :return: The softmax-transformed array, where values represent
             probabilities and sum to 1 over the input dimensions.
    :rtype: np.ndarray
    """
    return purem.softmax(arr)


def softmax_numpy(arr: np.ndarray) -> np.ndarray:
    """
    Compute the softmax of a given array using numpy.

    This function calculates the softmax of the input array. It stabilizes
    the computation by subtracting the maximum value from the array to
    prevent numerical overflow in the exponential calculation.

    :param arr: Input array for which softmax needs to be calculated.
    :type arr: np.ndarray
    :return: The computed softmax values as an array.
    :rtype: np.ndarray
    """
    arr_max = np.max(arr)
    shifted = arr - arr_max
    arr_exp = np.exp(shifted)
    return arr_exp / np.sum(arr_exp)


# device = "mps" if torch.backends.mps.is_available() else "cpu"
device = "cpu"


def softmax_torch(arr: np.ndarray) -> torch.Tensor:
    """
    Applies the softmax function along the last dimension of the input array.

    This function takes a NumPy array or similar Python data structure as input,
    converts it to a PyTorch tensor, and computes the softmax values along the
    last dimension. The input is cast to a float32 tensor for compatibility with
    PyTorch computations. The result is returned as a PyTorch tensor.

    :param arr: Input array to be converted to a PyTorch tensor and processed
        using the softmax function. The array can be of any shape, with the
        softmax computation applied along its last dimension.
    :type arr: array-like
    :return: A PyTorch tensor containing the softmax values computed from the
        input array along its last dimension.
    :rtype: torch.Tensor
    """
    array_torch = torch.asarray(arr, dtype=torch.float32, device=device)
    arr_torch = array_torch.to(dtype=torch.float32)
    return arr_torch.softmax(dim=-1)


@njit(parallel=True, fastmath=True)
def softmax_numba(arr: np.ndarray) -> np.ndarray:
    """
    Computes the softmax function for a given array with NumPy and Numba for
    parallelized execution. The function calculates the exponential normalization
    of a NumPy array to transform input values into probabilities that sum to one.

    The function optimizes the computation using the maximum value of the input
    array to improve numerical stability, specifically by avoiding overflow
    and underflow during exponentiation.

    :param arr: Input array of floats
    :type arr: numpy.ndarray
    :return: Array of softmax probabilities corresponding to the input
    :rtype: numpy.ndarray
    """
    n = arr.shape[0]
    out = np.empty_like(arr)
    x_max = -np.inf
    for i in range(n):
        if arr[i] > x_max:
            x_max = arr[i]
    total = 0.0
    for i in range(n):
        out[i] = math.exp(arr[i] - x_max)
        total += out[i]
    for i in range(n):
        out[i] /= total
    return out


functions = {
    'Softmax: Purem': softmax_purem,
    'Softmax: NumPy': softmax_numpy,
    'Softmax: PyTorch': softmax_torch,
    'Softmax: Numba': softmax_numba,
}


@pytest.mark.parametrize("func_name", [pytest.param("Softmax: Purem")])
def test_purem_softmax(benchmark, func_name, array_instance):
    """
    Tests the performance of the `softmax_purem` function using the Benchmark fixture.

    :param benchmark: Benchmark fixture provided by pytest-benchmark for measuring
        the performance of the `softmax_purem` function.
    :param func_name: Name of the function being tested in the benchmarking process.
    :param array_instance: Input data instance provided to the `softmax_purem` function
        during the benchmarking test.
    :return: None
    """
    benchmark(softmax_purem, array_instance)


@pytest.mark.parametrize("func_name", [pytest.param("Softmax: NumPy")])
def test_numpy_softmax(benchmark, func_name, array_instance):
    """
    Benchmark test for the numpy implementation of the softmax function. This test
    uses pytest parametrize to evaluate the performance of the function across
    different sizes of input and under a specific function name label. It clears
    garbage, generates random floating-point numbers of the specified size, and
    then benchmarks the softmax function.

    :param benchmark: The pytest-benchmark fixture used for executing the
        performance test of the softmax function.
    :param func_name: Determines the function name label for benchmarking.
    :param size: The size of the input data array to be generated and used in
        the softmax function.
    :return: None
    """
    benchmark(softmax_numpy, array_instance)


@pytest.mark.parametrize("func_name", [pytest.param("Softmax: PyTorch")])
def test_torch_softmax(benchmark, func_name, array_instance):
    """
    Tests the performance of the PyTorch implementation of the softmax function over
    an array of a specified size using the pytest benchmarking framework.

    This test generates a random array of numbers of the given size, applies the
    softmax operation implemented in PyTorch, and records its performance metrics
    using the pytest-benchmark plugin.

    :param benchmark: A pytest-benchmark function used to measure the performance
        of the specified function under test.
    :param func_name: The name of the function being tested.
    :param size: The size of the input array for which softmax computation is executed.
    :rtype: None
    :return: Returns None as this function is a test case invoked by pytest.
    """
    benchmark(softmax_torch, array_instance)


@pytest.mark.parametrize("func_name", [pytest.param("Softmax: Numba")])
def test_numba_softmax(benchmark, func_name, array_instance):
    """
    Tests the performance of the Numba-accelerated softmax function using pytest.

    This test leverages the pytest benchmarking tool to measure the time taken
    to compute the softmax operation on an array of specified size using the
    Numba implementation. It uses parameterized inputs for varying
    function names and sizes to validate the performance in different configurations.

    :param benchmark: The pytest benchmarking fixture used to time the execution
                      of the softmax function.
    :param func_name: The name of the function being tested. This parameter is
                      parameterized by the pytest decorator.
    :param size: The size of the input array for which the softmax function is
                 benchmarked. This parameter is also parameterized by the
                 pytest decorator.
    :return: None
    """
    benchmark(softmax_numba, array_instance)