pmpp 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,851 @@
1
+ """
2
+ Author: Nicolas Payot
3
+ Date: 06/18/2024
4
+
5
+ This Python script handles large scale Fourier Transforms utilizing multiple GPUs for computation. In particular, it
6
+ provides the functionality to distribute an ndarray across multiple GPUs for efficient computation of the Fourier
7
+ Transform.
8
+
9
+ The primary functions are split_array_for_gpus, distribute_array_on_gpus, create_sharded_fft, and create_ffts which
10
+ facilitate array division, allocation of array segments to GPUs, creating sharded versions of an FFT function, and
11
+ creating the main FFT functions for complex and real input that work in a distributed fashion across GPUs. Also, there
12
+ is a test function to compare the output and performance of multi-GPU FFT against a reference FFT function.
13
+
14
+ These functions are highly beneficial when dealing with extremely large arrays where parallelization of computation can
15
+ lead to significant performance improvements. The code leverages both numpy and jax libraries for computation and
16
+ parallelization.
17
+
18
+ Note: The script expects a computing environment with multiple GPUs available.
19
+
20
+ """
21
+
22
+ import numpy as np
23
+ from typing import Callable, Tuple
24
+ from functools import partial
25
+
26
+ import jax
27
+ from jax import lax, Array
28
+ from jax import custom_vjp
29
+ from jax.experimental import mesh_utils
30
+ from jax.experimental.custom_partitioning import custom_partitioning
31
+ from jax.sharding import Mesh, PartitionSpec as P, NamedSharding
32
+ import jax.tree_util as tree
33
+ import jax.numpy as jnp
34
+
35
+
36
+ def split_array_for_gpus(array: np.ndarray, num_gpus: int, axis: int = 1) -> Array:
37
+ """Split an array into equal chunks for host-side GPU distribution.
38
+
39
+ Parameters
40
+ ----------
41
+ array : numpy.ndarray
42
+ Input host array.
43
+ num_gpus : int
44
+ Number of device chunks to create.
45
+ axis : int, optional
46
+ Axis to split.
47
+
48
+ Returns
49
+ -------
50
+ numpy.ndarray
51
+ Array of host chunks, one per target GPU.
52
+ """
53
+ # Split the array equally along the second dimension for each GPU
54
+ return jnp.array(jnp.array_split(array, num_gpus, axis=axis))
55
+
56
+
57
+ def distribute_array_on_gpus_old(array: np.ndarray, compute_mesh: Mesh, partition: P,
58
+ axis_name: str = "gpus") -> jnp.ndarray:
59
+ """
60
+ Distributes the given array across multiple GPUs for computation. The distribution follows a partition configuration
61
+ and an axis along which the array should be split for distribution.
62
+ ea
63
+ Args:
64
+ array (np.ndarray): The input array to be distributed.
65
+ compute_mesh (Mesh): The compute mesh that defines the layout of GPUs.
66
+ partition (P): The partition configuration for distributing the array.
67
+ axis_name (str): The axis along which the array needs to be split for distribution. Defaults to "gpus".
68
+
69
+ Returns:
70
+ jnp.ndarray: A Jax array distributed across multiple GPUs.
71
+
72
+ Note:
73
+ The function 'split_array_for_gpus' is used to split the array equally for each GPU before distributing. Ensure that
74
+ the number of GPUs evenly divides the dimension corresponding to `axis_name` of `array` which is governed by the 'partition' configuration.
75
+ """
76
+ # Get the number of GPUs
77
+ num_gpus = len(compute_mesh.devices)
78
+ # Split the array equally for each GPU
79
+ array_parts = split_array_for_gpus(array, num_gpus, partition.index(axis_name))
80
+ with compute_mesh:
81
+ # Distribute the array parts across different GPUs
82
+ array_parts_device = [jax.device_put(part, device) for part, device in zip(array_parts, compute_mesh.devices)]
83
+ array_distributed = jax.make_array_from_single_device_arrays(array.shape,
84
+ NamedSharding(compute_mesh, partition),
85
+ array_parts_device)
86
+ return array_distributed
87
+
88
+
89
+ def distribute_array_on_gpus(array: np.ndarray, compute_mesh: Mesh, partition: P) -> jnp.ndarray:
90
+ """Place a host array onto a compute mesh with explicit sharding.
91
+
92
+ Parameters
93
+ ----------
94
+ array : numpy.ndarray
95
+ Input host array already shaped consistently with ``partition``.
96
+ compute_mesh : Mesh
97
+ Device mesh defining the target sharding.
98
+ partition : PartitionSpec
99
+ Partition specification for the output array.
100
+
101
+ Returns
102
+ -------
103
+ jax.Array
104
+ Sharded array placed on ``compute_mesh``.
105
+ """
106
+ sharding = NamedSharding(compute_mesh, partition)
107
+ array_parts_device = [jax.device_put(array[i], device=d) for d, i in
108
+ sharding.addressable_devices_indices_map(array.shape).items()]
109
+ array_distributed = jax.make_array_from_single_device_arrays(array.shape, sharding, array_parts_device)
110
+ return array_distributed
111
+
112
+
113
+ def create_sharded_fft(basic_fft: Callable, partition_spec: P, global_mesh: Mesh = None):
114
+ """Wrap a local FFT kernel in JAX custom partitioning metadata.
115
+
116
+ Parameters
117
+ ----------
118
+ basic_fft : callable
119
+ Local FFT kernel operating on one logical shard layout.
120
+ partition_spec : PartitionSpec
121
+ Target sharding of the FFT input and output.
122
+ global_mesh : Mesh, optional
123
+ Fallback mesh used when the input sharding object does not carry one
124
+ directly.
125
+
126
+ Returns
127
+ -------
128
+ callable
129
+ FFT wrapper carrying custom partitioning rules.
130
+ """
131
+
132
+ @custom_partitioning
133
+ def sharded_fft(x):
134
+ """Applies the FFT function to the input array with sharding applied.
135
+
136
+ Parameters
137
+ ----------
138
+ x
139
+ Input array transformed by this FFT wrapper."""
140
+ return basic_fft(x)
141
+
142
+ def supported_sharding(sharding, shape):
143
+ """Returns a NamedSharding based on the input sharding and the partition specification.
144
+
145
+ Parameters
146
+ ----------
147
+ shape
148
+ Array shape used when deriving output sharding."""
149
+ if isinstance(sharding, NamedSharding):
150
+ mesh = sharding.mesh
151
+ else:
152
+ mesh = global_mesh if global_mesh else sharding.mesh
153
+ return NamedSharding(mesh, partition_spec)
154
+
155
+ def partition(mesh, arg_shapes, result_shape):
156
+ """Defines how to partition the FFT computation across the devices.
157
+
158
+ Parameters
159
+ ----------
160
+ mesh
161
+ JAX device mesh supplied to the custom-partitioning rule.
162
+ arg_shapes
163
+ Abstract input shapes supplied by JAX custom partitioning.
164
+ result_shape
165
+ Abstract result shape supplied by JAX custom partitioning."""
166
+ arg_shardings = tree.tree_map(lambda x: x.sharding, arg_shapes)
167
+ return mesh, basic_fft, supported_sharding(arg_shardings[0], arg_shapes[0]), (
168
+ supported_sharding(arg_shardings[0], arg_shapes[0]),)
169
+
170
+ def infer_sharding_from_operands(mesh, arg_shapes, result_shape):
171
+ """Infers the sharding of the output based on the input sharding.
172
+
173
+ Parameters
174
+ ----------
175
+ mesh
176
+ JAX device mesh supplied to the custom-partitioning rule.
177
+ arg_shapes
178
+ Abstract input shapes supplied by JAX custom partitioning.
179
+ result_shape
180
+ Abstract result shape supplied by JAX custom partitioning."""
181
+ arg_shardings = tree.tree_map(lambda x: x.sharding, arg_shapes)
182
+ return supported_sharding(arg_shardings[0], arg_shapes[0])
183
+
184
+ sharded_fft.def_partition(
185
+ infer_sharding_from_operands=infer_sharding_from_operands,
186
+ partition=partition
187
+ )
188
+ return sharded_fft
189
+
190
+
191
+ def _fftn_first_pass(x):
192
+ """ Perform the FFT along the first two axes. """
193
+ return jnp.fft.fft(x, axis=0)
194
+
195
+
196
+ def _fftn_second_pass(x):
197
+ """ Perform the FFT along the last axis using FFT due to symmetry. """
198
+ return jnp.fft.fftn(x, axes=[1, 2])
199
+
200
+
201
+ def _rfftn_second_pass(x):
202
+ """ Perform the real FFT along the last axis using real FFT due to symmetry. """
203
+ return jnp.fft.rfftn(x, axes=[1, 2])
204
+
205
+
206
+ def _ifftn_first_pass(x):
207
+ """ Perform the inverse FFT along the first two axes. """
208
+ return jnp.fft.ifft(x, axis=0)
209
+
210
+
211
+ def _ifftn_second_pass(x):
212
+ """ Perform the inverse FFT along the last axis using FFT due to symmetry. """
213
+ return jnp.fft.ifftn(x, axes=[1, 2])
214
+
215
+
216
+ def _irfftn_second_pass(x):
217
+ """ Perform the real inverse FFT along the last axis using real FFT due to symmetry. """
218
+ return jnp.fft.irfftn(x, axes=[1, 2])
219
+
220
+
221
+ def _batched_fftn_first_pass(x):
222
+ """Perform the batched FFT along the distributed x-axis."""
223
+ return jnp.fft.fft(x, axis=1)
224
+
225
+
226
+ def _batched_rfftn_second_pass(x):
227
+ """Perform the batched real FFT on the local y/z planes."""
228
+ return jnp.fft.rfftn(x, axes=[2, 3])
229
+
230
+
231
+ def _batched_ifftn_first_pass(x):
232
+ """Perform the batched inverse FFT along the distributed x-axis."""
233
+ return jnp.fft.ifft(x, axis=1)
234
+
235
+
236
+ def _batched_irfftn_second_pass(x):
237
+ """Perform the batched real inverse FFT on the local y/z planes."""
238
+ return jnp.fft.irfftn(x, axes=[2, 3])
239
+
240
+
241
+ def create_batched_transposed_real_ffts(compute_mesh: Mesh) -> Tuple[Callable, Callable]:
242
+ """Create batched real FFT helpers for the transposed spectral layout.
243
+
244
+ Parameters
245
+ ----------
246
+ compute_mesh : Mesh
247
+ Device mesh over which the transposed spectral layout is sharded.
248
+
249
+ Returns
250
+ -------
251
+ tuple[callable, callable]
252
+ Batched forward and inverse real FFT helpers for the transposed layout.
253
+
254
+ Notes
255
+ -----
256
+ The leading axis is treated as a pure batch dimension and remains
257
+ replicated.
258
+ """
259
+ if compute_mesh.size == 1:
260
+ _batched_rfftn_transposed_jit = jax.jit(lambda x: _batched_fftn_first_pass(_batched_rfftn_second_pass(x)))
261
+ _batched_irfftn_transposed_jit = jax.jit(lambda x: _batched_irfftn_second_pass(_batched_ifftn_first_pass(x)))
262
+
263
+ @custom_vjp
264
+ def batched_irfftn_transposed(x):
265
+ """Apply the batched transposed-layout inverse real FFT.
266
+
267
+ Parameters
268
+ ----------
269
+ x
270
+ Input array transformed by this FFT wrapper.
271
+ """
272
+ return _batched_irfftn_transposed_jit(x)
273
+
274
+ def batched_irfftn_transposed_fwd(x):
275
+ """Save the batched transposed inverse FFT input for its custom VJP.
276
+
277
+ Parameters
278
+ ----------
279
+ x
280
+ Input array transformed by this FFT wrapper.
281
+ """
282
+ return _batched_irfftn_transposed_jit(x), x.shape
283
+
284
+ def batched_irfftn_transposed_bwd(res, g):
285
+ """Apply the adjoint of the batched transposed inverse real FFT.
286
+
287
+ Parameters
288
+ ----------
289
+ res
290
+ Residual values saved by a custom VJP forward rule.
291
+ g
292
+ Cotangent array supplied to a custom VJP backward rule.
293
+ """
294
+ real_shape = g.shape
295
+ g = _batched_rfftn_transposed_jit(g).conj()
296
+ n = g.shape[-1]
297
+ is_even = (real_shape[-1] % 2 == 0)
298
+
299
+ mask = jnp.ones((n,), dtype=g.real.dtype)
300
+ if n > 1:
301
+ mask = mask.at[1:].set(2.0)
302
+ if is_even:
303
+ mask = mask.at[n - 1].set(1.0)
304
+
305
+ scale = jnp.asarray(1 / np.prod(real_shape[1:]), dtype=g.real.dtype)
306
+ out = scale * g * lax.expand_dims(mask, range(g.ndim - 1))
307
+ return (out,)
308
+
309
+ batched_irfftn_transposed.defvjp(
310
+ batched_irfftn_transposed_fwd,
311
+ batched_irfftn_transposed_bwd,
312
+ )
313
+ return _batched_rfftn_transposed_jit, batched_irfftn_transposed
314
+
315
+ batched_fftn_first_pass = create_sharded_fft(
316
+ _batched_fftn_first_pass,
317
+ P(None, None, "gpus", None),
318
+ compute_mesh,
319
+ )
320
+ batched_rfftn_second_pass = create_sharded_fft(
321
+ _batched_rfftn_second_pass,
322
+ P(None, "gpus", None, None),
323
+ compute_mesh,
324
+ )
325
+ batched_ifftn_first_pass = create_sharded_fft(
326
+ _batched_ifftn_first_pass,
327
+ P(None, None, "gpus", None),
328
+ compute_mesh,
329
+ )
330
+ batched_irfftn_second_pass = create_sharded_fft(
331
+ _batched_irfftn_second_pass,
332
+ P(None, "gpus", None, None),
333
+ compute_mesh,
334
+ )
335
+
336
+ def _batched_rfftn_transposed(x):
337
+ x = batched_rfftn_second_pass(x)
338
+ x = batched_fftn_first_pass(x)
339
+ return x
340
+
341
+ def _batched_irfftn_transposed(x):
342
+ x = batched_ifftn_first_pass(x)
343
+ x = batched_irfftn_second_pass(x)
344
+ return x
345
+
346
+ _batched_rfftn_transposed_jit = jax.jit(
347
+ _batched_rfftn_transposed,
348
+ in_shardings=(NamedSharding(compute_mesh, P(None, "gpus", None, None))),
349
+ out_shardings=(NamedSharding(compute_mesh, P(None, None, "gpus", None))),
350
+ )
351
+ _batched_irfftn_transposed_jit = jax.jit(
352
+ _batched_irfftn_transposed,
353
+ in_shardings=(NamedSharding(compute_mesh, P(None, None, "gpus", None))),
354
+ out_shardings=(NamedSharding(compute_mesh, P(None, "gpus", None, None))),
355
+ )
356
+
357
+ @custom_vjp
358
+ def batched_irfftn_transposed(x):
359
+ """Apply the batched transposed-layout inverse real FFT.
360
+
361
+ Parameters
362
+ ----------
363
+ x
364
+ Input array transformed by this FFT wrapper.
365
+ """
366
+ return _batched_irfftn_transposed_jit(x)
367
+
368
+ def batched_irfftn_transposed_fwd(x):
369
+ """Save the batched transposed inverse FFT input for its custom VJP.
370
+
371
+ Parameters
372
+ ----------
373
+ x
374
+ Input array transformed by this FFT wrapper.
375
+ """
376
+ return _batched_irfftn_transposed_jit(x), x.shape
377
+
378
+ def batched_irfftn_transposed_bwd(res, g):
379
+ """Apply the adjoint of the batched transposed inverse real FFT.
380
+
381
+ Parameters
382
+ ----------
383
+ res
384
+ Residual values saved by a custom VJP forward rule.
385
+ g
386
+ Cotangent array supplied to a custom VJP backward rule.
387
+ """
388
+ real_shape = g.shape
389
+ g = _batched_rfftn_transposed_jit(g).conj()
390
+ n = g.shape[-1]
391
+ is_even = (real_shape[-1] % 2 == 0)
392
+
393
+ mask = jnp.ones((n,), dtype=g.real.dtype)
394
+ if n > 1:
395
+ mask = mask.at[1:].set(2.0)
396
+ if is_even:
397
+ mask = mask.at[n - 1].set(1.0)
398
+
399
+ scale = jnp.asarray(1 / np.prod(real_shape[1:]), dtype=g.real.dtype)
400
+ out = scale * g * lax.expand_dims(mask, range(g.ndim - 1))
401
+ return (out,)
402
+
403
+ batched_irfftn_transposed.defvjp(
404
+ batched_irfftn_transposed_fwd,
405
+ batched_irfftn_transposed_bwd,
406
+ )
407
+
408
+ return _batched_rfftn_transposed_jit, batched_irfftn_transposed
409
+
410
+
411
+ def create_ffts(compute_mesh: Mesh) -> Tuple[Callable, Callable, Callable, Callable, Callable, Callable]:
412
+ """Create the distributed FFT helper family used by PM++.
413
+
414
+ Parameters
415
+ ----------
416
+ compute_mesh : Mesh
417
+ Device mesh defining the parallel FFT layout.
418
+
419
+ Returns
420
+ -------
421
+ tuple[callable, callable, callable, callable, callable, callable]
422
+ ``(rfftn, irfftn, fftn, ifftn, rfftn_transposed, irfftn_transposed)``
423
+ helpers specialized to ``compute_mesh``.
424
+
425
+ Notes
426
+ -----
427
+ The real transforms carry custom VJPs so they remain differentiable in the
428
+ PM++ solver. The complex transforms are utility kernels without the same
429
+ public differentiation contract.
430
+ """
431
+ # Single-device shortcut: bypass distributed 2-pass FFT entirely.
432
+ # The strided-batched cuFFT plan created by the first pass is incompatible
433
+ # with some single-GPU hardware (e.g. GTX 1650 / cuFFT 12).
434
+ if compute_mesh.size == 1:
435
+ _fftn_jit = jax.jit(jnp.fft.fftn)
436
+ _ifftn_jit = jax.jit(jnp.fft.ifftn)
437
+ _rfftn_jit = jax.jit(jnp.fft.rfftn)
438
+ _irfftn_jit = jax.jit(jnp.fft.irfftn)
439
+
440
+ @custom_vjp
441
+ def rfftn(x):
442
+ """Apply the distributed real-to-complex FFT wrapper.
443
+
444
+ Parameters
445
+ ----------
446
+ x
447
+ Input array transformed by this FFT wrapper.
448
+ """
449
+ return _rfftn_jit(x)
450
+
451
+ def rfftn_fwd(x):
452
+ """Save the real FFT input shape for its custom VJP.
453
+
454
+ Parameters
455
+ ----------
456
+ x
457
+ Input array transformed by this FFT wrapper.
458
+ """
459
+ return _rfftn_jit(x), x.shape
460
+
461
+ def rfftn_bwd(x_shape, g):
462
+ """Apply the inverse transform used by the real FFT custom VJP.
463
+
464
+ Parameters
465
+ ----------
466
+ x_shape
467
+ Original real-space input shape saved for an inverse custom VJP.
468
+ g
469
+ Cotangent array supplied to a custom VJP backward rule.
470
+ """
471
+ g = jnp.pad(g, [(0, si - xi) for xi, si in zip(g.shape, x_shape)])
472
+ g = _ifftn_jit(g.conj()).real
473
+ g *= jnp.prod(jnp.array(x_shape))
474
+ return (g,)
475
+
476
+ rfftn.defvjp(rfftn_fwd, rfftn_bwd)
477
+
478
+ @custom_vjp
479
+ def irfftn(x):
480
+ """Apply the distributed complex-to-real inverse FFT wrapper.
481
+
482
+ Parameters
483
+ ----------
484
+ x
485
+ Input array transformed by this FFT wrapper.
486
+ """
487
+ return _irfftn_jit(x)
488
+
489
+ def irfftn_fwd(x):
490
+ """Save the inverse real FFT input for its custom VJP.
491
+
492
+ Parameters
493
+ ----------
494
+ x
495
+ Input array transformed by this FFT wrapper.
496
+ """
497
+ return _irfftn_jit(x), x.shape
498
+
499
+ def irfftn_bwd(res, g):
500
+ """Apply the real FFT adjoint for the inverse real FFT custom VJP.
501
+
502
+ Parameters
503
+ ----------
504
+ res
505
+ Residual values saved by a custom VJP forward rule.
506
+ g
507
+ Cotangent array supplied to a custom VJP backward rule.
508
+ """
509
+ real_shape = g.shape
510
+ g = _rfftn_jit(g).conj()
511
+ n = g.shape[-1]
512
+ is_even = (real_shape[-1] % 2 == 0)
513
+ mask = jnp.ones((n,), dtype=g.real.dtype)
514
+ if n > 1:
515
+ mask = mask.at[1:].set(2.0)
516
+ if is_even:
517
+ mask = mask.at[n - 1].set(1.0)
518
+ scale = jnp.asarray(1 / np.prod(real_shape), dtype=g.real.dtype)
519
+ out = scale * g * lax.expand_dims(mask, range(g.ndim - 1))
520
+ return (out,)
521
+
522
+ irfftn.defvjp(irfftn_fwd, irfftn_bwd)
523
+
524
+ return rfftn, irfftn, _fftn_jit, _ifftn_jit, rfftn, irfftn
525
+
526
+ # Creating sharded versions of FFT and IFFT functions for specific GPU sharding
527
+ fftn_first_pass = create_sharded_fft(_fftn_first_pass, P(None, "gpus", None), compute_mesh)
528
+ fftn_second_pass = create_sharded_fft(_fftn_second_pass, P("gpus", None, None), compute_mesh)
529
+ rfftn_second_pass = create_sharded_fft(_rfftn_second_pass, P("gpus", None, None), compute_mesh)
530
+ ifftn_first_pass = create_sharded_fft(_ifftn_first_pass, P(None, "gpus", None), compute_mesh)
531
+ ifftn_second_pass = create_sharded_fft(_ifftn_second_pass, P("gpus", None, None), compute_mesh)
532
+ irfftn_second_pass = create_sharded_fft(_irfftn_second_pass, P("gpus", None, None), compute_mesh)
533
+
534
+ def _fftn(x):
535
+ """ Perform a forward FFT. """
536
+ x = fftn_second_pass(x)
537
+ x = fftn_first_pass(x)
538
+ return x
539
+
540
+ def _ifftn(x):
541
+ """ Perform an inverse FFT. """
542
+ x = ifftn_first_pass(x)
543
+ x = ifftn_second_pass(x)
544
+ return x
545
+
546
+ def _rfftn(x):
547
+ """ Perform a real-valued forward FFT. """
548
+ x = rfftn_second_pass(x)
549
+ x = fftn_first_pass(x)
550
+ return x
551
+
552
+ def _irfftn(x):
553
+ """ Perform a real-valued inverse FFT. """
554
+ x = ifftn_first_pass(x)
555
+ x = irfftn_second_pass(x)
556
+ return x
557
+
558
+ # Creating jitted versions of FFT and IFFT functions for specific GPU sharding
559
+ _rfftn_jit = jax.jit(_rfftn, in_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))),
560
+ out_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))))
561
+ _irfftn_jit = jax.jit(_irfftn, in_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))),
562
+ out_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))))
563
+ _fftn_jit = jax.jit(_fftn, in_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))),
564
+ out_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))))
565
+ _ifftn_jit = jax.jit(_ifftn, in_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))),
566
+ out_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))))
567
+ # Natural spectral layout: keep the intermediate transpose produced by the
568
+ # x-axis FFT so downstream spectral operators can avoid bouncing layouts.
569
+ _rfftn_transposed_jit = jax.jit(
570
+ _rfftn,
571
+ in_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))),
572
+ out_shardings=(NamedSharding(compute_mesh, P(None, "gpus", None))),
573
+ )
574
+ _irfftn_transposed_jit = jax.jit(
575
+ _irfftn,
576
+ in_shardings=(NamedSharding(compute_mesh, P(None, "gpus", None))),
577
+ out_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))),
578
+ )
579
+ _fftn_transposed_jit = jax.jit(
580
+ _fftn,
581
+ in_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))),
582
+ out_shardings=(NamedSharding(compute_mesh, P(None, "gpus", None))),
583
+ )
584
+ _ifftn_transposed_jit = jax.jit(
585
+ _ifftn,
586
+ in_shardings=(NamedSharding(compute_mesh, P(None, "gpus", None))),
587
+ out_shardings=(NamedSharding(compute_mesh, P("gpus", None, None))),
588
+ )
589
+
590
+ @custom_vjp
591
+ def rfftn(x):
592
+ """Perform a real-valued forward FFT with custom VJP (vector-Jacobian product) defined.
593
+
594
+ Note:
595
+ This function is differentiable and the derivative is defined using VJP.
596
+
597
+ Parameters
598
+ ----------
599
+ x
600
+ Input array transformed by this FFT wrapper."""
601
+ return _rfftn_jit(x)
602
+
603
+ def rfftn_fwd(x):
604
+ """Forward pass for custom VJP of real-valued forward FFT
605
+
606
+ Parameters
607
+ ----------
608
+ x
609
+ Input array transformed by this FFT wrapper."""
610
+ return _rfftn_jit(x), x.shape
611
+
612
+ def rfftn_bwd(x_shape, g):
613
+ """Backward pass for custom VJP of real-valued forward FFT
614
+
615
+ Parameters
616
+ ----------
617
+ x_shape
618
+ Original real-space input shape saved for an inverse custom VJP.
619
+ g
620
+ Cotangent array supplied to a custom VJP backward rule."""
621
+ g = jnp.pad(g, [(0, si - xi) for xi, si in zip(g.shape, x_shape)])
622
+ g = _ifftn_jit(g.conj()).real
623
+ # the previous code is equivalent to jnp.fft.ifftn(g.conj(), s=x_shape).real
624
+ g *= jnp.prod(jnp.array(x_shape))
625
+ return (g,)
626
+
627
+ rfftn.defvjp(rfftn_fwd, rfftn_bwd)
628
+
629
+ @custom_vjp
630
+ def rfftn_transposed(x):
631
+ """Apply the distributed real FFT and return transposed spectral layout.
632
+
633
+ Parameters
634
+ ----------
635
+ x
636
+ Input array transformed by this FFT wrapper.
637
+ """
638
+ return _rfftn_transposed_jit(x)
639
+
640
+ def rfftn_transposed_fwd(x):
641
+ """Save the transposed real FFT input shape for its custom VJP.
642
+
643
+ Parameters
644
+ ----------
645
+ x
646
+ Input array transformed by this FFT wrapper.
647
+ """
648
+ return _rfftn_transposed_jit(x), x.shape
649
+
650
+ def rfftn_transposed_bwd(x_shape, g):
651
+ """Apply the adjoint of the transposed real FFT.
652
+
653
+ Parameters
654
+ ----------
655
+ x_shape
656
+ Original real-space input shape saved for an inverse custom VJP.
657
+ g
658
+ Cotangent array supplied to a custom VJP backward rule.
659
+ """
660
+ g = jnp.pad(g, [(0, si - xi) for xi, si in zip(g.shape, x_shape)])
661
+ g = _ifftn_transposed_jit(g.conj()).real
662
+ g *= jnp.prod(jnp.array(x_shape))
663
+ return (g,)
664
+
665
+ rfftn_transposed.defvjp(rfftn_transposed_fwd, rfftn_transposed_bwd)
666
+
667
+ @custom_vjp
668
+ def irfftn(x):
669
+ """Perform a real-valued inverse FFT with custom VJP (vector-Jacobian product) defined.
670
+
671
+ Note:
672
+ This function is differentiable and the derivative is defined using VJP.
673
+
674
+ Parameters
675
+ ----------
676
+ x
677
+ Input array transformed by this FFT wrapper."""
678
+ return _irfftn_jit(x)
679
+
680
+ def irfftn_fwd(x):
681
+ """Forward pass for custom VJP of real-valued inverse FFT
682
+
683
+ Parameters
684
+ ----------
685
+ x
686
+ Input array transformed by this FFT wrapper."""
687
+ return _irfftn_jit(x), x.shape
688
+
689
+ def irfftn_bwd(res, g):
690
+ """Backward pass for custom VJP of real-valued inverse FFT
691
+
692
+ Parameters
693
+ ----------
694
+ res
695
+ Residual values saved by a custom VJP forward rule.
696
+ g
697
+ Cotangent array supplied to a custom VJP backward rule."""
698
+ real_shape = g.shape
699
+ # Compute the RFFT of the gradient (changes the size of g)
700
+ g = _rfftn_jit(g).conj() # jnp.fft.rfftn(g) # _rfftn_jit(g)
701
+ # Apply the scaling factor and mask
702
+ n = g.shape[-1]
703
+ is_even = (real_shape[-1] % 2 == 0)
704
+
705
+ # The FFT shape is static under JIT, so the Hermitian weighting can be built
706
+ # directly without padding to an external max size.
707
+ mask = jnp.ones((n,), dtype=g.real.dtype)
708
+ if n > 1:
709
+ mask = mask.at[1:].set(2.0)
710
+ if is_even:
711
+ mask = mask.at[n - 1].set(1.0)
712
+
713
+ scale = jnp.asarray(1 / np.prod(real_shape), dtype=g.real.dtype)
714
+ out = scale * g * lax.expand_dims(mask, range(g.ndim - 1))
715
+ return (out,)
716
+
717
+ irfftn.defvjp(irfftn_fwd, irfftn_bwd)
718
+
719
+ @custom_vjp
720
+ def irfftn_transposed(x):
721
+ """Apply the inverse real FFT from transposed spectral layout.
722
+
723
+ Parameters
724
+ ----------
725
+ x
726
+ Input array transformed by this FFT wrapper.
727
+ """
728
+ return _irfftn_transposed_jit(x)
729
+
730
+ def irfftn_transposed_fwd(x):
731
+ """Save the transposed inverse FFT input for its custom VJP.
732
+
733
+ Parameters
734
+ ----------
735
+ x
736
+ Input array transformed by this FFT wrapper.
737
+ """
738
+ return _irfftn_transposed_jit(x), x.shape
739
+
740
+ def irfftn_transposed_bwd(res, g):
741
+ """Apply the adjoint of the transposed inverse real FFT.
742
+
743
+ Parameters
744
+ ----------
745
+ res
746
+ Residual values saved by a custom VJP forward rule.
747
+ g
748
+ Cotangent array supplied to a custom VJP backward rule.
749
+ """
750
+ real_shape = g.shape
751
+ g = _rfftn_transposed_jit(g).conj()
752
+ n = g.shape[-1]
753
+ is_even = (real_shape[-1] % 2 == 0)
754
+
755
+ mask = jnp.ones((n,), dtype=g.real.dtype)
756
+ if n > 1:
757
+ mask = mask.at[1:].set(2.0)
758
+ if is_even:
759
+ mask = mask.at[n - 1].set(1.0)
760
+
761
+ scale = jnp.asarray(1 / np.prod(real_shape), dtype=g.real.dtype)
762
+ out = scale * g * lax.expand_dims(mask, range(g.ndim - 1))
763
+ return (out,)
764
+
765
+ irfftn_transposed.defvjp(irfftn_transposed_fwd, irfftn_transposed_bwd)
766
+
767
+ return rfftn, irfftn, _fftn_jit, _ifftn_jit, rfftn_transposed, irfftn_transposed
768
+
769
+
770
+ def test_functions(distributed_func_jit: Callable, reference_func: Callable, array: np.ndarray, mesh: Mesh) -> Tuple[
771
+ bool, float, float]:
772
+ """
773
+ Tests the accuracy and computational performance of two functions. It does this by comparing the output of
774
+ a distributed function (that's been Just-in-Time compiled) provided as "distributed_func_jit" and a reference function.
775
+ It reports whether the outputs of both functions are close (within a certain tolerance), the maximum absolute difference
776
+ in their outputs, and the value at which the maximum absolute difference occurs in the reference function output.
777
+
778
+ Args:
779
+ distributed_func_jit (Callable): A function that distributes its computations across multiple devices and has
780
+ been Just-In-Time compiled. This is the function being tested.
781
+ reference_func (Callable): A reference function that is used to compare the accuracy and performance of the
782
+ distributed function.
783
+ array (np.ndarray): The input numpy array that both functions will use to compute their outputs.
784
+ mesh (Mesh) : The compute mesh that defines the layout of devices for distributed computation.
785
+
786
+ Returns:
787
+ tuple: A tuple containing the following elements:
788
+ - all_close (bool): A boolean indicating if the outputs of both functions are close within a relative tolerance of
789
+ 1.e-2 and absolute tolerance of 1.e-4.
790
+ - max_diff (float): The maximum absolute difference in the outputs of both functions.
791
+ - max_diff_value_ref_func (float): The value in the output of the reference function at which the maximum absolute
792
+ difference occurs.
793
+
794
+ Note:
795
+ If the maximum absolute difference is non-zero, also prints the maximum relative difference and the corresponding value
796
+ in the output of the reference function.
797
+ """
798
+ print(f"\nTests for function: {reference_func.__name__}.\n")
799
+ array_distributed = distribute_array_on_gpus(array, mesh, P(None, "gpus", None))
800
+ reference_func_jit = jax.jit(reference_func)
801
+ with mesh:
802
+ distributed_func_jit_out = distributed_func_jit(array_distributed)
803
+ reference_func_jit_out = reference_func_jit(array)
804
+
805
+ all_close = np.allclose(distributed_func_jit_out, reference_func_jit_out, rtol=1.e-2, atol=1.e-4)
806
+ diff = reference_func_jit_out - distributed_func_jit_out
807
+ max_diff = np.max(np.abs(diff))
808
+ max_diff_index = np.unravel_index(np.argmax(np.abs(diff)), diff.shape)
809
+ max_diff_value_ref = reference_func_jit_out[max_diff_index]
810
+
811
+ print(f"\n{'Output close to reference' if all_close else 'WARNING: Output not close to reference'}")
812
+ print(f"{max_diff = }")
813
+ print(f"{max_diff_value_ref = }")
814
+
815
+ if max_diff != 0:
816
+ relative_diff = np.abs(diff) / np.maximum(np.abs(distributed_func_jit_out), np.abs(reference_func_jit_out))
817
+ max_relative_diff = np.max(relative_diff)
818
+ max_relative_diff_index = np.unravel_index(np.argmax(relative_diff), relative_diff.shape)
819
+ max_relative_diff_value_ref = reference_func_jit_out[max_relative_diff_index]
820
+
821
+ print(f"{max_relative_diff = }")
822
+ print(f"{max_relative_diff_value_ref = }")
823
+
824
+
825
+ if __name__ == '__main__':
826
+ import os
827
+
828
+ num_gpus = jax.device_count()
829
+ print("CUDA_VISIBLE_DEVICES", os.environ.get("CUDA_VISIBLE_DEVICES"))
830
+ print("devices:", jax.device_count(), jax.devices())
831
+ print("local_devices:", jax.local_device_count(), jax.local_devices())
832
+ print("process_index", jax.process_index())
833
+ print("total number of GPUs:", num_gpus)
834
+
835
+ devices = mesh_utils.create_device_mesh((num_gpus,), devices=jax.devices()[:num_gpus])
836
+ mesh = Mesh(devices, axis_names=("gpus",))
837
+
838
+ rfftn_jit, irfftn_jit, fftn_jit, ifftn_jit = create_ffts(mesh)
839
+
840
+ jax.config.update("jax_enable_x64", True)
841
+
842
+ size = 512
843
+ global_shape = (size, size, size)
844
+ x = np.random.default_rng(2).random(global_shape, dtype=np.float64)
845
+
846
+ x_distributed = distribute_array_on_gpus(x, mesh, P(None, "gpus", None))
847
+
848
+ with mesh:
849
+ rfftn_out = rfftn_jit(x_distributed)
850
+
851
+ test_functions(rfftn_jit, jnp.fft.rfftn, x, mesh)