1
1
import math
2
+ import platform
2
3
import shutil
4
+ import sys
3
5
from functools import partial , reduce
4
6
5
7
import pytest
6
8
7
- from cubed .core .ops import partial_reduce
8
- from cubed .core .optimization import multiple_inputs_optimize_dag
9
-
10
- pytest .importorskip ("lithops" )
11
-
12
9
import cubed
13
10
import cubed .array_api as xp
14
11
import cubed .random
15
12
from cubed .backend_array_api import namespace as nxp
13
+ from cubed .core .ops import partial_reduce
14
+ from cubed .core .optimization import multiple_inputs_optimize_dag
16
15
from cubed .extensions .history import HistoryCallback
17
16
from cubed .extensions .mem_warn import MemoryWarningCallback
18
- from cubed .runtime .executors . lithops import LithopsExecutor
17
+ from cubed .runtime .create import create_executor
19
18
from cubed .tests .utils import LITHOPS_LOCAL_CONFIG
20
19
20
+ ALLOWED_MEM = 2_000_000_000
21
+
22
+ EXECUTORS = {}
23
+
24
+ if platform .system () != "Windows" :
25
+ EXECUTORS ["processes" ] = create_executor ("processes" )
26
+
27
+ # Run with max_tasks_per_child=1 so that each task is run in a new process,
28
+ # allowing us to perform a stronger check on peak memory
29
+ if sys .version_info >= (3 , 11 ):
30
+ executor_options = dict (max_tasks_per_child = 1 )
31
+ EXECUTORS ["processes-single-task" ] = create_executor (
32
+ "processes" , executor_options
33
+ )
34
+
35
+ try :
36
+ executor_options = dict (config = LITHOPS_LOCAL_CONFIG , wait_dur_sec = 0.1 )
37
+ EXECUTORS ["lithops" ] = create_executor ("lithops" , executor_options )
38
+ except ImportError :
39
+ pass
40
+
21
41
22
42
@pytest .fixture ()
23
43
def spec (tmp_path , reserved_mem ):
24
- return cubed .Spec (tmp_path , allowed_mem = 2_000_000_000 , reserved_mem = reserved_mem )
44
+ return cubed .Spec (tmp_path , allowed_mem = ALLOWED_MEM , reserved_mem = reserved_mem )
45
+
46
+
47
+ @pytest .fixture (
48
+ scope = "module" ,
49
+ params = EXECUTORS .values (),
50
+ ids = EXECUTORS .keys (),
51
+ )
52
+ def executor (request ):
53
+ return request .param
25
54
26
55
27
56
@pytest .fixture (scope = "module" )
28
- def reserved_mem ():
29
- executor = LithopsExecutor (config = LITHOPS_LOCAL_CONFIG )
57
+ def reserved_mem (executor ):
30
58
res = cubed .measure_reserved_mem (executor ) * 1.1 # add some wiggle room
31
59
return round_up_to_multiple (res , 10_000_000 ) # round up to nearest multiple of 10MB
32
60
@@ -40,58 +68,58 @@ def round_up_to_multiple(x, multiple=10):
40
68
41
69
42
70
@pytest .mark .slow
43
- def test_index (tmp_path , spec ):
71
+ def test_index (tmp_path , spec , executor ):
44
72
a = cubed .random .random (
45
73
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
46
74
) # 200MB chunks
47
75
b = a [1 :, :]
48
- run_operation (tmp_path , "index" , b )
76
+ run_operation (tmp_path , executor , "index" , b )
49
77
50
78
51
79
@pytest .mark .slow
52
- def test_index_step (tmp_path , spec ):
80
+ def test_index_step (tmp_path , spec , executor ):
53
81
a = cubed .random .random (
54
82
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
55
83
) # 200MB chunks
56
84
b = a [::2 , :]
57
- run_operation (tmp_path , "index_step" , b )
85
+ run_operation (tmp_path , executor , "index_step" , b )
58
86
59
87
60
88
# Creation Functions
61
89
62
90
63
91
@pytest .mark .slow
64
- def test_eye (tmp_path , spec ):
92
+ def test_eye (tmp_path , spec , executor ):
65
93
a = xp .eye (10000 , 10000 , chunks = (5000 , 5000 ), spec = spec )
66
- run_operation (tmp_path , "eye" , a )
94
+ run_operation (tmp_path , executor , "eye" , a )
67
95
68
96
69
97
@pytest .mark .slow
70
- def test_tril (tmp_path , spec ):
98
+ def test_tril (tmp_path , spec , executor ):
71
99
a = cubed .random .random (
72
100
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
73
101
) # 200MB chunks
74
102
b = xp .tril (a )
75
- run_operation (tmp_path , "tril" , b )
103
+ run_operation (tmp_path , executor , "tril" , b )
76
104
77
105
78
106
# Elementwise Functions
79
107
80
108
81
109
@pytest .mark .slow
82
- def test_add (tmp_path , spec ):
110
+ def test_add (tmp_path , spec , executor ):
83
111
a = cubed .random .random (
84
112
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
85
113
) # 200MB chunks
86
114
b = cubed .random .random (
87
115
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
88
116
) # 200MB chunks
89
117
c = xp .add (a , b )
90
- run_operation (tmp_path , "add" , c )
118
+ run_operation (tmp_path , executor , "add" , c )
91
119
92
120
93
121
@pytest .mark .slow
94
- def test_add_reduce_left (tmp_path , spec ):
122
+ def test_add_reduce_left (tmp_path , spec , executor ):
95
123
# Perform the `add` operation repeatedly on pairs of arrays, also known as fold left.
96
124
# See https://en.wikipedia.org/wiki/Fold_(higher-order_function)
97
125
#
@@ -111,11 +139,13 @@ def test_add_reduce_left(tmp_path, spec):
111
139
]
112
140
result = reduce (lambda x , y : xp .add (x , y ), arrs )
113
141
opt_fn = partial (multiple_inputs_optimize_dag , max_total_source_arrays = n_arrays * 2 )
114
- run_operation (tmp_path , "add_reduce_left" , result , optimize_function = opt_fn )
142
+ run_operation (
143
+ tmp_path , executor , "add_reduce_left" , result , optimize_function = opt_fn
144
+ )
115
145
116
146
117
147
@pytest .mark .slow
118
- def test_add_reduce_right (tmp_path , spec ):
148
+ def test_add_reduce_right (tmp_path , spec , executor ):
119
149
# Perform the `add` operation repeatedly on pairs of arrays, also known as fold right.
120
150
# See https://en.wikipedia.org/wiki/Fold_(higher-order_function)
121
151
#
@@ -137,23 +167,25 @@ def test_add_reduce_right(tmp_path, spec):
137
167
]
138
168
result = reduce (lambda x , y : xp .add (y , x ), reversed (arrs ))
139
169
opt_fn = partial (multiple_inputs_optimize_dag , max_total_source_arrays = n_arrays * 2 )
140
- run_operation (tmp_path , "add_reduce_right" , result , optimize_function = opt_fn )
170
+ run_operation (
171
+ tmp_path , executor , "add_reduce_right" , result , optimize_function = opt_fn
172
+ )
141
173
142
174
143
175
@pytest .mark .slow
144
- def test_negative (tmp_path , spec ):
176
+ def test_negative (tmp_path , spec , executor ):
145
177
a = cubed .random .random (
146
178
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
147
179
) # 200MB chunks
148
180
b = xp .negative (a )
149
- run_operation (tmp_path , "negative" , b )
181
+ run_operation (tmp_path , executor , "negative" , b )
150
182
151
183
152
184
# Linear Algebra Functions
153
185
154
186
155
187
@pytest .mark .slow
156
- def test_matmul (tmp_path , spec ):
188
+ def test_matmul (tmp_path , spec , executor ):
157
189
a = cubed .random .random (
158
190
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
159
191
) # 200MB chunks
@@ -163,20 +195,20 @@ def test_matmul(tmp_path, spec):
163
195
c = xp .astype (a , xp .float32 )
164
196
d = xp .astype (b , xp .float32 )
165
197
e = xp .matmul (c , d )
166
- run_operation (tmp_path , "matmul" , e )
198
+ run_operation (tmp_path , executor , "matmul" , e )
167
199
168
200
169
201
@pytest .mark .slow
170
- def test_matrix_transpose (tmp_path , spec ):
202
+ def test_matrix_transpose (tmp_path , spec , executor ):
171
203
a = cubed .random .random (
172
204
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
173
205
) # 200MB chunks
174
206
b = xp .matrix_transpose (a )
175
- run_operation (tmp_path , "matrix_transpose" , b )
207
+ run_operation (tmp_path , executor , "matrix_transpose" , b )
176
208
177
209
178
210
@pytest .mark .slow
179
- def test_tensordot (tmp_path , spec ):
211
+ def test_tensordot (tmp_path , spec , executor ):
180
212
a = cubed .random .random (
181
213
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
182
214
) # 200MB chunks
@@ -186,14 +218,14 @@ def test_tensordot(tmp_path, spec):
186
218
c = xp .astype (a , xp .float32 )
187
219
d = xp .astype (b , xp .float32 )
188
220
e = xp .tensordot (c , d , axes = 1 )
189
- run_operation (tmp_path , "tensordot" , e )
221
+ run_operation (tmp_path , executor , "tensordot" , e )
190
222
191
223
192
224
# Manipulation Functions
193
225
194
226
195
227
@pytest .mark .slow
196
- def test_concat (tmp_path , spec ):
228
+ def test_concat (tmp_path , spec , executor ):
197
229
# Note 'a' has one fewer element in axis=0 to force chunking to cross array boundaries
198
230
a = cubed .random .random (
199
231
(9999 , 10000 ), chunks = (5000 , 5000 ), spec = spec
@@ -202,81 +234,80 @@ def test_concat(tmp_path, spec):
202
234
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
203
235
) # 200MB chunks
204
236
c = xp .concat ((a , b ), axis = 0 )
205
- run_operation (tmp_path , "concat" , c )
237
+ run_operation (tmp_path , executor , "concat" , c )
206
238
207
239
208
240
@pytest .mark .slow
209
- def test_reshape (tmp_path , spec ):
241
+ def test_reshape (tmp_path , spec , executor ):
210
242
a = cubed .random .random (
211
243
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
212
244
) # 200MB chunks
213
245
# need intermediate reshape due to limitations in Dask's reshape_rechunk
214
246
b = xp .reshape (a , (5000 , 2 , 10000 ))
215
247
c = xp .reshape (b , (5000 , 20000 ))
216
- run_operation (tmp_path , "reshape" , c )
248
+ run_operation (tmp_path , executor , "reshape" , c )
217
249
218
250
219
251
@pytest .mark .slow
220
- def test_stack (tmp_path , spec ):
252
+ def test_stack (tmp_path , spec , executor ):
221
253
a = cubed .random .random (
222
254
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
223
255
) # 200MB chunks
224
256
b = cubed .random .random (
225
257
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
226
258
) # 200MB chunks
227
259
c = xp .stack ((a , b ), axis = 0 )
228
- run_operation (tmp_path , "stack" , c )
260
+ run_operation (tmp_path , executor , "stack" , c )
229
261
230
262
231
263
# Searching Functions
232
264
233
265
234
266
@pytest .mark .slow
235
- def test_argmax (tmp_path , spec ):
267
+ def test_argmax (tmp_path , spec , executor ):
236
268
a = cubed .random .random (
237
269
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
238
270
) # 200MB chunks
239
271
b = xp .argmax (a , axis = 0 )
240
- run_operation (tmp_path , "argmax" , b )
272
+ run_operation (tmp_path , executor , "argmax" , b )
241
273
242
274
243
275
# Statistical Functions
244
276
245
277
246
278
@pytest .mark .slow
247
- def test_max (tmp_path , spec ):
279
+ def test_max (tmp_path , spec , executor ):
248
280
a = cubed .random .random (
249
281
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
250
282
) # 200MB chunks
251
283
b = xp .max (a , axis = 0 )
252
- run_operation (tmp_path , "max" , b )
284
+ run_operation (tmp_path , executor , "max" , b )
253
285
254
286
255
287
@pytest .mark .slow
256
- def test_mean (tmp_path , spec ):
288
+ def test_mean (tmp_path , spec , executor ):
257
289
a = cubed .random .random (
258
290
(10000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
259
291
) # 200MB chunks
260
292
b = xp .mean (a , axis = 0 )
261
- run_operation (tmp_path , "mean" , b )
293
+ run_operation (tmp_path , executor , "mean" , b )
262
294
263
295
264
296
@pytest .mark .slow
265
- def test_sum_partial_reduce (tmp_path , spec ):
297
+ def test_sum_partial_reduce (tmp_path , spec , executor ):
266
298
a = cubed .random .random (
267
299
(40000 , 10000 ), chunks = (5000 , 5000 ), spec = spec
268
300
) # 200MB chunks
269
301
b = partial_reduce (a , nxp .sum , split_every = {0 : 8 })
270
- run_operation (tmp_path , "sum_partial_reduce" , b )
302
+ run_operation (tmp_path , executor , "sum_partial_reduce" , b )
271
303
272
304
273
305
# Internal functions
274
306
275
307
276
- def run_operation (tmp_path , name , result_array , * , optimize_function = None ):
308
+ def run_operation (tmp_path , executor , name , result_array , * , optimize_function = None ):
277
309
# result_array.visualize(f"cubed-{name}-unoptimized", optimize_graph=False)
278
310
# result_array.visualize(f"cubed-{name}", optimize_function=optimize_function)
279
- executor = LithopsExecutor (config = LITHOPS_LOCAL_CONFIG )
280
311
hist = HistoryCallback ()
281
312
mem_warn = MemoryWarningCallback ()
282
313
# use store=None to write to temporary zarr
@@ -291,8 +322,19 @@ def run_operation(tmp_path, name, result_array, *, optimize_function=None):
291
322
df = hist .stats_df
292
323
print (df )
293
324
325
+ # check peak memory does not exceed allowed mem
326
+ assert (df ["peak_measured_mem_end_mb_max" ] <= ALLOWED_MEM // 1_000_000 ).all ()
327
+
328
+ # check change in peak memory is no more than projected mem
329
+ assert (df ["peak_measured_mem_delta_mb_max" ] <= df ["projected_mem_mb" ]).all ()
330
+
294
331
# check projected_mem_utilization does not exceed 1
295
- assert (df ["projected_mem_utilization" ] <= 1.0 ).all ()
332
+ # except on processes executor that runs multiple tasks in a process
333
+ if (
334
+ executor .name != "processes"
335
+ or executor .kwargs .get ("max_tasks_per_child" , None ) == 1
336
+ ):
337
+ assert (df ["projected_mem_utilization" ] <= 1.0 ).all ()
296
338
297
339
# delete temp files for this test immediately since they are so large
298
340
shutil .rmtree (tmp_path )
0 commit comments