Kernels
chrisvoncsefalvay commited on
Commit
4796c24
·
verified ·
1 Parent(s): 0f28b15

Publish HOLYSHT kernel bundle (53048d01ccae)

Browse files
Files changed (48) hide show
  1. .gitattributes +11 -35
  2. LICENSE +21 -0
  3. README.md +91 -0
  4. build.toml +38 -0
  5. build/torch210-cxx11-cu126-x86_64-linux/__init__.py +673 -0
  6. build/torch210-cxx11-cu126-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
  7. build/torch210-cxx11-cu126-x86_64-linux/_jit_ops.py +72 -0
  8. build/torch210-cxx11-cu126-x86_64-linux/_ops.py +9 -0
  9. build/torch210-cxx11-cu126-x86_64-linux/holysht/__init__.py +26 -0
  10. build/torch210-cxx11-cu126-x86_64-linux/metadata.json +9 -0
  11. build/torch210-cxx11-cu128-x86_64-linux/__init__.py +673 -0
  12. build/torch210-cxx11-cu128-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
  13. build/torch210-cxx11-cu128-x86_64-linux/_jit_ops.py +72 -0
  14. build/torch210-cxx11-cu128-x86_64-linux/_ops.py +9 -0
  15. build/torch210-cxx11-cu128-x86_64-linux/holysht/__init__.py +26 -0
  16. build/torch210-cxx11-cu128-x86_64-linux/metadata.json +12 -0
  17. build/torch210-cxx11-cu130-x86_64-linux/__init__.py +673 -0
  18. build/torch210-cxx11-cu130-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
  19. build/torch210-cxx11-cu130-x86_64-linux/_jit_ops.py +72 -0
  20. build/torch210-cxx11-cu130-x86_64-linux/_ops.py +9 -0
  21. build/torch210-cxx11-cu130-x86_64-linux/holysht/__init__.py +26 -0
  22. build/torch210-cxx11-cu130-x86_64-linux/metadata.json +12 -0
  23. build/torch211-cxx11-cu126-x86_64-linux/__init__.py +673 -0
  24. build/torch211-cxx11-cu126-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
  25. build/torch211-cxx11-cu126-x86_64-linux/_jit_ops.py +72 -0
  26. build/torch211-cxx11-cu126-x86_64-linux/_ops.py +9 -0
  27. build/torch211-cxx11-cu126-x86_64-linux/holysht/__init__.py +26 -0
  28. build/torch211-cxx11-cu126-x86_64-linux/metadata.json +9 -0
  29. build/torch211-cxx11-cu128-x86_64-linux/__init__.py +673 -0
  30. build/torch211-cxx11-cu128-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
  31. build/torch211-cxx11-cu128-x86_64-linux/_jit_ops.py +72 -0
  32. build/torch211-cxx11-cu128-x86_64-linux/_ops.py +9 -0
  33. build/torch211-cxx11-cu128-x86_64-linux/holysht/__init__.py +26 -0
  34. build/torch211-cxx11-cu128-x86_64-linux/metadata.json +12 -0
  35. build/torch211-cxx11-cu130-x86_64-linux/__init__.py +673 -0
  36. build/torch211-cxx11-cu130-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
  37. build/torch211-cxx11-cu130-x86_64-linux/_jit_ops.py +72 -0
  38. build/torch211-cxx11-cu130-x86_64-linux/_ops.py +9 -0
  39. build/torch211-cxx11-cu130-x86_64-linux/holysht/__init__.py +26 -0
  40. build/torch211-cxx11-cu130-x86_64-linux/metadata.json +12 -0
  41. build/torch29-cxx11-cu129-x86_64-linux/__init__.py +673 -0
  42. build/torch29-cxx11-cu129-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
  43. build/torch29-cxx11-cu129-x86_64-linux/_jit_ops.py +72 -0
  44. build/torch29-cxx11-cu129-x86_64-linux/_ops.py +9 -0
  45. build/torch29-cxx11-cu129-x86_64-linux/holysht/__init__.py +26 -0
  46. build/torch29-cxx11-cu129-x86_64-linux/metadata.json +12 -0
  47. flake.lock +117 -0
  48. flake.nix +17 -0
.gitattributes CHANGED
@@ -1,35 +1,11 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.so binary
2
+ *.dylib binary
3
+ *.pyd binary
4
+ *.dll binary
5
+ build/torch210-cxx11-cu126-x86_64-linux/_holysht_cuda_53048d0.abi3.so filter=lfs diff=lfs merge=lfs -text
6
+ build/torch210-cxx11-cu128-x86_64-linux/_holysht_cuda_53048d0.abi3.so filter=lfs diff=lfs merge=lfs -text
7
+ build/torch210-cxx11-cu130-x86_64-linux/_holysht_cuda_53048d0.abi3.so filter=lfs diff=lfs merge=lfs -text
8
+ build/torch211-cxx11-cu126-x86_64-linux/_holysht_cuda_53048d0.abi3.so filter=lfs diff=lfs merge=lfs -text
9
+ build/torch211-cxx11-cu128-x86_64-linux/_holysht_cuda_53048d0.abi3.so filter=lfs diff=lfs merge=lfs -text
10
+ build/torch211-cxx11-cu130-x86_64-linux/_holysht_cuda_53048d0.abi3.so filter=lfs diff=lfs merge=lfs -text
11
+ build/torch29-cxx11-cu129-x86_64-linux/_holysht_cuda_53048d0.abi3.so filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris von Csefalvay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HOLYSHT card
2
+
3
+ Author: Chris von Csefalvay
4
+ Licence: MIT
5
+ Repository: https://github.com/chrisvoncsefalvay/holysht
6
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
7
+
8
+ ## Name
9
+
10
+ **HOLYSHT**: highly optimised Legendre/Ylm/SHT
11
+
12
+ ## Summary
13
+
14
+ HOLYSHT is a focused CUDA acceleration layer for spherical harmonic transforms
15
+ in the `torch-harmonics` ecosystem. It accelerates the Legendre stage, vector
16
+ SHT composition, and inverse-FFT preparation rather than replacing the whole
17
+ upstream library.
18
+
19
+ ## Scope
20
+
21
+ Included:
22
+
23
+ - Scalar forward SHT
24
+ - Scalar inverse SHT
25
+ - Vector forward SHT
26
+ - Vector inverse SHT
27
+ - Explicit backward support for the custom scalar and vector kernels
28
+ - BF16 forward paths backed by CUDA real-reduction kernels
29
+ - Benchmark, profiling, and parity-test tooling
30
+
31
+ Intentionally excluded:
32
+
33
+ - The earlier DISCO fusion work
34
+ - Experimental side-project code
35
+ - Heavy multi-architecture kernel-builder fan-out in normal development
36
+
37
+ ## Benchmark summary
38
+
39
+ Measured on 11 April 2026 with PyTorch `2.10.0+cu130`, CUDA `13.0`, batch size
40
+ `4`, an allocation cap of `6 GiB`, and an NVIDIA GB10:
41
+
42
+ - Scalar forward: up to `4.6x`
43
+ - Scalar inverse: up to `2.0x`
44
+ - Vector forward: up to `8.7x`
45
+ - Vector inverse: up to `8.7x`
46
+ - Scalar forward + backward: `2.3x`
47
+ - Vector forward + backward: `4.3x`
48
+ - BF16 scalar forward: `1.6x`
49
+ - BF16 vector forward: `1.6x`
50
+
51
+ All `20/20` executed correctness checks passed in that quick sweep.
52
+
53
+ ## Core techniques
54
+
55
+ 1. Architecture-aware launch selection between `tile_l=4` and `tile_l=8`.
56
+ 2. Shared-memory large-grid kernels for scalar, vector, and BF16 real paths.
57
+ 3. Dedicated vector forward and inverse kernels to remove the old Python-side
58
+ packing hot path.
59
+ 4. Real-valued BF16 Legendre reductions with float accumulation.
60
+ 5. Explicit autograd wrappers for scalar and vector CUDA paths.
61
+ 6. CUDA-side `irfft` preparation to avoid extra Python tensor passes.
62
+ 7. Local torch JIT build caching under `build/torch_extensions` for safer
63
+ day-to-day iteration on GB10.
64
+
65
+ ## Resource snapshot
66
+
67
+ From `cuobjdump --dump-resource-usage` on the local `sm_120` build:
68
+
69
+ - Scalar forward large kernel: `38` registers/thread, `3136` B shared/block,
70
+ `256` threads/block, `6` active blocks/SM.
71
+ - Vector forward large kernel: `37` registers/thread, `5248` B shared/block,
72
+ `256` threads/block, `6` active blocks/SM.
73
+ - BF16 forward large kernel: `34` registers/thread, `2080` B shared/block,
74
+ `256` threads/block, `6` active blocks/SM.
75
+ - `prepare_irfft`: `19` registers/thread, no shared memory.
76
+
77
+ ## Practical constraints
78
+
79
+ - HOLYSHT still depends on `torch-harmonics` for quadrature weight generation.
80
+ - The default local build targets `12.0+PTX` on GB10-class systems.
81
+ - `ncu` may require elevated GPU counter permissions; the supplied script falls
82
+ back to `cuobjdump` resource reporting when those counters are unavailable.
83
+ - Very large inverse-heavy cases should still be run under an allocation cap on
84
+ unified-memory systems.
85
+
86
+ ## Intended audience
87
+
88
+ - Researchers already using `torch-harmonics` who want faster SHT execution.
89
+ - Neural operator workloads where SHT dominates forward or training cost.
90
+ - CUDA developers who want a small, inspectable codebase with real profiling and
91
+ resource-report hooks rather than hand-wavy speed claims.
build.toml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HOLYSHT
2
+ # Author: Chris von Csefalvay
3
+ # Licence: MIT
4
+ # Repository: https://github.com/chrisvoncsefalvay/holysht
5
+ # Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
6
+
7
+ [general]
8
+ name = "holysht"
9
+ version = 1
10
+ license = "mit"
11
+ upstream = "https://github.com/NVIDIA/torch-harmonics"
12
+ backends = ["cuda"]
13
+
14
+ [general.hub]
15
+ repo-id = "chrisvoncsefalvay/holysht"
16
+
17
+ [torch]
18
+ src = [
19
+ "torch-ext/torch_binding.cpp",
20
+ "torch-ext/torch_binding.h",
21
+ "torch-ext/registration_select.h",
22
+ "torch-ext/local_registration.h",
23
+ ]
24
+ pyext = ["py"]
25
+
26
+ [kernel.holysht_legendre]
27
+ backend = "cuda"
28
+ depends = ["torch"]
29
+ src = ["cuda/fused_legendre.cu"]
30
+ cuda-capabilities = ["12.0"]
31
+ cuda-flags = ["-O3", "-lineinfo", "-Xptxas=-warn-spills", "--use_fast_math"]
32
+
33
+ [kernel.holysht_sht]
34
+ backend = "cuda"
35
+ depends = ["torch"]
36
+ src = ["cuda/fused_sht.cu"]
37
+ cuda-capabilities = ["12.0"]
38
+ cuda-flags = ["-O3", "-lineinfo", "-Xptxas=-warn-spills", "--use_fast_math"]
build/torch210-cxx11-cu126-x86_64-linux/__init__.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HOLYSHT: Highly Optimised Legendre/Ylm/SHT.
2
+
3
+ CUDA-accelerated spherical harmonic transforms designed as a practical,
4
+ production-oriented companion to torch-harmonics.
5
+
6
+ Author: Chris von Csefalvay
7
+ Licence: MIT
8
+ Repository: https://github.com/chrisvoncsefalvay/holysht
9
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
10
+ """
11
+
12
+ import contextlib
13
+ import os
14
+ from typing import Optional
15
+ import torch
16
+ import torch.nn as nn
17
+
18
+ __all__ = [
19
+ "RealSHT",
20
+ "InverseRealSHT",
21
+ "RealVectorSHT",
22
+ "InverseRealVectorSHT",
23
+ "legendre_forward",
24
+ "legendre_inverse",
25
+ "sht_forward",
26
+ "sht_inverse",
27
+ ]
28
+
29
+ # Prefer kernel-builder's generated alias module on packaged builds, then fall
30
+ # back to the local single-machine JIT loader for development.
31
+ try:
32
+ from ._ops import ops as _ops
33
+ _HAS_CUDA_EXT = True
34
+ except ModuleNotFoundError:
35
+ try:
36
+ from ._jit_ops import ops as _ops
37
+ _HAS_CUDA_EXT = True
38
+ except ImportError:
39
+ _HAS_CUDA_EXT = False
40
+ except ImportError:
41
+ _HAS_CUDA_EXT = False
42
+
43
+
44
+ def _can_use_cuda_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
45
+ return (
46
+ _HAS_CUDA_EXT
47
+ and weight_t is not None
48
+ and input.is_cuda
49
+ and weight_t.is_cuda
50
+ and input.dtype == torch.complex64
51
+ and weight_t.dtype == torch.float32
52
+ and weight_t.is_contiguous()
53
+ )
54
+
55
+
56
+ def _can_use_cuda_real_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
57
+ return (
58
+ _HAS_CUDA_EXT
59
+ and weight_t is not None
60
+ and input.is_cuda
61
+ and weight_t.is_cuda
62
+ and input.dtype in (torch.float32, torch.bfloat16)
63
+ and weight_t.dtype == torch.float32
64
+ and input.is_contiguous()
65
+ and weight_t.is_contiguous()
66
+ )
67
+
68
+
69
+ def _can_use_cuda_vector(input: torch.Tensor, weight0_t: Optional[torch.Tensor], weight1_t: Optional[torch.Tensor]) -> bool:
70
+ return (
71
+ _HAS_CUDA_EXT
72
+ and weight0_t is not None
73
+ and weight1_t is not None
74
+ and input.is_cuda
75
+ and weight0_t.is_cuda
76
+ and weight1_t.is_cuda
77
+ and input.dtype == torch.complex64
78
+ and weight0_t.dtype == torch.float32
79
+ and weight1_t.dtype == torch.float32
80
+ and input.is_contiguous()
81
+ and weight0_t.is_contiguous()
82
+ and weight1_t.is_contiguous()
83
+ )
84
+
85
+
86
+ def _mul_i(x: torch.Tensor) -> torch.Tensor:
87
+ """Multiply a complex tensor by +i without promoting dtype."""
88
+ return torch.complex(-x.imag, x.real)
89
+
90
+
91
+ @contextlib.contextmanager
92
+ def _nvtx_range(name: str):
93
+ enabled = os.environ.get("HOLYSHT_ENABLE_NVTX", "0") == "1"
94
+ if enabled and torch.cuda.is_available():
95
+ torch.cuda.nvtx.range_push(name)
96
+ try:
97
+ yield
98
+ finally:
99
+ torch.cuda.nvtx.range_pop()
100
+ else:
101
+ yield
102
+
103
+
104
+ def _prepare_irfft_input(x: torch.Tensor, nlon: int, active_mmax: Optional[int] = None) -> torch.Tensor:
105
+ """Pad/clean an rFFT-format complex tensor before irfft."""
106
+ active_mmax = x.size(-1) if active_mmax is None else active_mmax
107
+ full_mmax = nlon // 2 + 1
108
+
109
+ if x.size(-1) == full_mmax:
110
+ out = x.contiguous()
111
+ else:
112
+ out_shape = list(x.shape)
113
+ out_shape[-1] = full_mmax
114
+ out = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
115
+ out[..., :x.size(-1)] = x
116
+
117
+ if _HAS_CUDA_EXT and out.is_cuda and out.dtype == torch.complex64:
118
+ orig_shape = out.shape
119
+ flat = out.reshape(-1, orig_shape[-2], orig_shape[-1]).contiguous()
120
+ _ops.sht_prepare_irfft(flat, active_mmax, nlon)
121
+ return flat.reshape(orig_shape)
122
+
123
+ out[..., 0] = out[..., 0].real.to(torch.complex64)
124
+ if nlon % 2 == 0:
125
+ nyquist_idx = nlon // 2
126
+ if nyquist_idx < out.size(-1):
127
+ out[..., nyquist_idx] = out[..., nyquist_idx].real.to(torch.complex64)
128
+ return out
129
+
130
+
131
+ class _FusedLegendreForwardFn(torch.autograd.Function):
132
+ @staticmethod
133
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
134
+ input_c = input.contiguous()
135
+ output = torch.empty(
136
+ input_c.size(0), weight_t.size(0), input_c.size(2),
137
+ device=input_c.device, dtype=torch.complex64
138
+ )
139
+ _ops.fused_legendre_forward(output, input_c, weight_t)
140
+ ctx.save_for_backward(weight_t)
141
+ return output
142
+
143
+ @staticmethod
144
+ def backward(ctx, grad_output: torch.Tensor):
145
+ (weight_t,) = ctx.saved_tensors
146
+ grad_output = grad_output.contiguous()
147
+ grad_input = torch.empty(
148
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
149
+ device=grad_output.device, dtype=torch.complex64
150
+ )
151
+ _ops.fused_legendre_inverse(grad_input, grad_output, weight_t)
152
+ return grad_input, None
153
+
154
+
155
+ class _FusedLegendreInverseFn(torch.autograd.Function):
156
+ @staticmethod
157
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
158
+ input_c = input.contiguous()
159
+ output = torch.empty(
160
+ input_c.size(0), weight_t.size(1), input_c.size(2),
161
+ device=input_c.device, dtype=torch.complex64
162
+ )
163
+ _ops.fused_legendre_inverse(output, input_c, weight_t)
164
+ ctx.save_for_backward(weight_t)
165
+ return output
166
+
167
+ @staticmethod
168
+ def backward(ctx, grad_output: torch.Tensor):
169
+ (weight_t,) = ctx.saved_tensors
170
+ grad_output = grad_output.contiguous()
171
+ grad_input = torch.empty(
172
+ grad_output.size(0), weight_t.size(0), grad_output.size(2),
173
+ device=grad_output.device, dtype=torch.complex64
174
+ )
175
+ _ops.fused_legendre_forward(grad_input, grad_output, weight_t)
176
+ return grad_input, None
177
+
178
+
179
+ class _FusedLegendreForwardRealFn(torch.autograd.Function):
180
+ @staticmethod
181
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
182
+ input_c = input.contiguous()
183
+ output = torch.empty(
184
+ input_c.size(0), weight_t.size(0), input_c.size(2),
185
+ device=input_c.device, dtype=torch.float32
186
+ )
187
+ _ops.fused_legendre_forward_real(output, input_c, weight_t)
188
+ ctx.save_for_backward(weight_t)
189
+ return output
190
+
191
+ @staticmethod
192
+ def backward(ctx, grad_output: torch.Tensor):
193
+ (weight_t,) = ctx.saved_tensors
194
+ grad_output = grad_output.contiguous()
195
+ grad_input = torch.empty(
196
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
197
+ device=grad_output.device, dtype=torch.float32
198
+ )
199
+ _ops.fused_legendre_inverse_real(grad_input, grad_output, weight_t)
200
+ return grad_input, None
201
+
202
+
203
+ class _FusedVectorLegendreForwardFn(torch.autograd.Function):
204
+ @staticmethod
205
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
206
+ input_c = input.contiguous()
207
+ output = torch.empty(
208
+ input_c.size(0), 2, weight0_t.size(0), input_c.size(3),
209
+ device=input_c.device, dtype=torch.complex64
210
+ )
211
+ _ops.fused_vector_legendre_forward(output, input_c, weight0_t, weight1_t)
212
+ ctx.save_for_backward(weight0_t, weight1_t)
213
+ return output
214
+
215
+ @staticmethod
216
+ def backward(ctx, grad_output: torch.Tensor):
217
+ weight0_t, weight1_t = ctx.saved_tensors
218
+ grad_output = grad_output.contiguous()
219
+ grad_input = torch.empty(
220
+ grad_output.size(0), 2, weight0_t.size(1), grad_output.size(3),
221
+ device=grad_output.device, dtype=torch.complex64
222
+ )
223
+ _ops.fused_vector_legendre_inverse(grad_input, grad_output, weight0_t, weight1_t)
224
+ return grad_input, None, None
225
+
226
+
227
+ class _FusedVectorLegendreInverseFn(torch.autograd.Function):
228
+ @staticmethod
229
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
230
+ input_c = input.contiguous()
231
+ output = torch.empty(
232
+ input_c.size(0), 2, weight0_t.size(1), input_c.size(3),
233
+ device=input_c.device, dtype=torch.complex64
234
+ )
235
+ _ops.fused_vector_legendre_inverse(output, input_c, weight0_t, weight1_t)
236
+ ctx.save_for_backward(weight0_t, weight1_t)
237
+ return output
238
+
239
+ @staticmethod
240
+ def backward(ctx, grad_output: torch.Tensor):
241
+ weight0_t, weight1_t = ctx.saved_tensors
242
+ grad_output = grad_output.contiguous()
243
+ grad_input = torch.empty(
244
+ grad_output.size(0), 2, weight0_t.size(0), grad_output.size(3),
245
+ device=grad_output.device, dtype=torch.complex64
246
+ )
247
+ _ops.fused_vector_legendre_forward(grad_input, grad_output, weight0_t, weight1_t)
248
+ return grad_input, None, None
249
+
250
+
251
+ # ============================================================================
252
+ # Fused Legendre Transform
253
+ # ============================================================================
254
+
255
+ def fused_legendre_forward(
256
+ input: torch.Tensor, # [B, nlat, mmax] complex64
257
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32 (original torch-harmonics layout)
258
+ weight_t: Optional[torch.Tensor] = None, # [lmax, nlat, mmax] pre-transposed
259
+ ) -> torch.Tensor:
260
+ """Fused forward Legendre transform operating on complex tensors.
261
+
262
+ Computes out[b,l,m] = Σ_k weights[m,l,k] · input[b,k,m] for complex input,
263
+ fusing the real and imaginary multiplications into a single pass.
264
+
265
+ Uses the custom CUDA kernels when the extension is available, otherwise
266
+ falls back to a stacked einsum.
267
+ """
268
+ B = input.size(0)
269
+ nlat = input.size(1)
270
+ mmax = input.size(2)
271
+ lmax = weights.size(1)
272
+
273
+ if weight_t is None:
274
+ weight_t = weights.float().permute(1, 2, 0).contiguous()
275
+
276
+ if _can_use_cuda_legendre(input, weight_t):
277
+ # Adaptive CUDA kernel: small-grid direct path + large-grid tiled path.
278
+ return _FusedLegendreForwardFn.apply(input, weight_t)
279
+ else:
280
+ # Fallback: stacked einsum (1.9x speedup over reference 2x einsum)
281
+ x = torch.view_as_real(input) # [B, nlat, mmax, 2]
282
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0) # [2B, nlat, mmax]
283
+ w = weights.to(x_stacked.dtype)
284
+ out_stacked = torch.einsum("bkm,mlk->blm", x_stacked, w)
285
+ out_re = out_stacked[:B]
286
+ out_im = out_stacked[B:]
287
+ return torch.complex(out_re, out_im)
288
+
289
+
290
+ def fused_legendre_inverse(
291
+ input: torch.Tensor, # [B, lmax, mmax] complex64
292
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
293
+ pct_t: Optional[torch.Tensor] = None,
294
+ ) -> torch.Tensor:
295
+ """Fused inverse Legendre transform."""
296
+ B = input.size(0)
297
+ lmax = input.size(1)
298
+ mmax = input.size(2)
299
+ nlat = pct.size(2)
300
+
301
+ if pct_t is None:
302
+ pct_t = pct.float().permute(1, 2, 0).contiguous()
303
+
304
+ if _can_use_cuda_legendre(input, pct_t):
305
+ return _FusedLegendreInverseFn.apply(input, pct_t)
306
+ else:
307
+ x = torch.view_as_real(input)
308
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0)
309
+ p = pct.to(x_stacked.dtype)
310
+ out_stacked = torch.einsum("blm,mlk->bkm", x_stacked, p)
311
+ return torch.complex(out_stacked[:B], out_stacked[B:])
312
+
313
+
314
+ def fused_legendre_forward_real(
315
+ input: torch.Tensor, # [B, nlat, mmax] float32 or bfloat16
316
+ weight_t: torch.Tensor, # [lmax, nlat, mmax] float32
317
+ ) -> torch.Tensor:
318
+ """Real-valued forward Legendre transform with float accumulation."""
319
+ if _can_use_cuda_real_legendre(input, weight_t):
320
+ return _FusedLegendreForwardRealFn.apply(input, weight_t)
321
+
322
+ return torch.einsum("bkm,lkm->blm", input.float(), weight_t)
323
+
324
+
325
+ # ============================================================================
326
+ # Fused SHT (complete pipeline)
327
+ # ============================================================================
328
+
329
+ def fused_sht_forward(
330
+ x: torch.Tensor, # [B, nlat, nlon] float32
331
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32
332
+ mmax: int,
333
+ weight_t: Optional[torch.Tensor] = None,
334
+ ) -> torch.Tensor:
335
+ """Complete fused forward SHT: rfft → fused Legendre → complex coefficients.
336
+
337
+ Replaces RealSHT.forward() with fewer intermediate allocations.
338
+ """
339
+ with _nvtx_range("holysht.scalar_forward"):
340
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
341
+ x_fft = x_fft[..., :mmax]
342
+ return fused_legendre_forward(x_fft, weights, weight_t)
343
+
344
+
345
+ def fused_sht_inverse(
346
+ coeffs: torch.Tensor, # [B, lmax, mmax] complex64
347
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
348
+ nlon: int,
349
+ pct_t: Optional[torch.Tensor] = None,
350
+ ) -> torch.Tensor:
351
+ """Complete fused inverse SHT: fused Legendre → zero-pad → irfft."""
352
+ with _nvtx_range("holysht.scalar_inverse"):
353
+ x = fused_legendre_inverse(coeffs, pct, pct_t)
354
+ x = _prepare_irfft_input(x, nlon, coeffs.size(-1))
355
+ return torch.fft.irfft(x, n=nlon, dim=-1, norm="forward")
356
+
357
+
358
+ # ============================================================================
359
+ # nn.Module wrappers (drop-in replacements for torch-harmonics)
360
+ # ============================================================================
361
+
362
+ class RealSHT(nn.Module):
363
+ """Optimised drop-in replacement for ``torch_harmonics.RealSHT``.
364
+
365
+ Args:
366
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ nlat: int,
372
+ nlon: int,
373
+ lmax: Optional[int] = None,
374
+ mmax: Optional[int] = None,
375
+ grid: str = "equiangular",
376
+ norm: str = "ortho",
377
+ csphase: bool = True,
378
+ dtype: str = "fp32",
379
+ ):
380
+ super().__init__()
381
+ from torch_harmonics import RealSHT
382
+ ref = RealSHT(
383
+ nlat,
384
+ nlon,
385
+ lmax=lmax,
386
+ mmax=mmax,
387
+ grid=grid,
388
+ norm=norm,
389
+ csphase=csphase,
390
+ )
391
+ self.nlat = ref.nlat
392
+ self.nlon = ref.nlon
393
+ self.lmax = ref.lmax
394
+ self.mmax = ref.mmax
395
+ self.grid = grid
396
+ self.norm = norm
397
+ self.csphase = csphase
398
+ self._use_bf16 = (dtype == "bf16")
399
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
400
+ self.register_buffer("weights", ref.weights.to(w_dtype))
401
+ self.register_buffer("weight_t", ref.weights.float().permute(1, 2, 0).contiguous())
402
+
403
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
404
+ if self._use_bf16:
405
+ with _nvtx_range("holysht.scalar_forward_bf16"):
406
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
407
+ x_fft = x_fft[..., :self.mmax]
408
+ xr = torch.view_as_real(x_fft)
409
+ if _HAS_CUDA_EXT and x.is_cuda and not x.requires_grad:
410
+ xr_bf16 = xr.bfloat16().contiguous()
411
+ out_re = fused_legendre_forward_real(xr_bf16[..., 0].contiguous(), self.weight_t)
412
+ out_im = fused_legendre_forward_real(xr_bf16[..., 1].contiguous(), self.weight_t)
413
+ return torch.complex(out_re, out_im)
414
+
415
+ B = x.size(0)
416
+ xs = torch.cat([xr[..., 0], xr[..., 1]], dim=0).bfloat16()
417
+ out = torch.einsum("bkm,mlk->blm", xs, self.weights).float()
418
+ return torch.complex(out[:B], out[B:])
419
+ return fused_sht_forward(x, self.weights, self.mmax, self.weight_t)
420
+
421
+
422
+ class InverseRealSHT(nn.Module):
423
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealSHT``."""
424
+
425
+ def __init__(
426
+ self,
427
+ nlat: int,
428
+ nlon: int,
429
+ lmax: Optional[int] = None,
430
+ mmax: Optional[int] = None,
431
+ grid: str = "equiangular",
432
+ norm: str = "ortho",
433
+ csphase: bool = True,
434
+ ):
435
+ super().__init__()
436
+ from torch_harmonics import InverseRealSHT
437
+ ref = InverseRealSHT(
438
+ nlat,
439
+ nlon,
440
+ lmax=lmax,
441
+ mmax=mmax,
442
+ grid=grid,
443
+ norm=norm,
444
+ csphase=csphase,
445
+ )
446
+ self.nlat = ref.nlat
447
+ self.nlon = ref.nlon
448
+ self.lmax = ref.lmax
449
+ self.mmax = ref.mmax
450
+ self.grid = grid
451
+ self.norm = norm
452
+ self.csphase = csphase
453
+ self.register_buffer("pct", ref.pct.float())
454
+ self.register_buffer("pct_t", ref.pct.float().permute(1, 2, 0).contiguous())
455
+
456
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
457
+ return fused_sht_inverse(x, self.pct, self.nlon, self.pct_t)
458
+
459
+
460
+ class RealVectorSHT(nn.Module):
461
+ """Optimised drop-in replacement for ``torch_harmonics.RealVectorSHT``.
462
+
463
+ Reduces eight reference einsums to two composed Legendre passes on the
464
+ default FP32 CUDA path.
465
+
466
+ Args:
467
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
468
+ """
469
+
470
+ def __init__(
471
+ self,
472
+ nlat: int,
473
+ nlon: int,
474
+ lmax: Optional[int] = None,
475
+ mmax: Optional[int] = None,
476
+ grid: str = "equiangular",
477
+ norm: str = "ortho",
478
+ csphase: bool = True,
479
+ dtype: str = "fp32",
480
+ ):
481
+ super().__init__()
482
+ from torch_harmonics import RealVectorSHT
483
+ ref = RealVectorSHT(
484
+ nlat,
485
+ nlon,
486
+ lmax=lmax,
487
+ mmax=mmax,
488
+ grid=grid,
489
+ norm=norm,
490
+ csphase=csphase,
491
+ )
492
+ self.nlat = ref.nlat
493
+ self.nlon = ref.nlon
494
+ self.lmax = ref.lmax
495
+ self.mmax = ref.mmax
496
+ self.grid = grid
497
+ self.norm = norm
498
+ self.csphase = csphase
499
+ self._use_bf16 = (dtype == "bf16")
500
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
501
+ self.register_buffer("w0", ref.weights[0].to(w_dtype)) # [mmax, lmax, nlat]
502
+ self.register_buffer("w1", ref.weights[1].to(w_dtype)) # [mmax, lmax, nlat]
503
+ self.register_buffer("w0_t", ref.weights[0].float().permute(1, 2, 0).contiguous())
504
+ self.register_buffer("w1_t", ref.weights[1].float().permute(1, 2, 0).contiguous())
505
+
506
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
507
+ assert x.shape[-2] == self.nlat and x.shape[-1] == self.nlon
508
+
509
+ with _nvtx_range("holysht.vector_forward"):
510
+ x = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
511
+ mmax = self.mmax
512
+ x = x[..., :mmax].contiguous()
513
+
514
+ if (not self._use_bf16) and _can_use_cuda_vector(x, self.w0_t, self.w1_t):
515
+ B_shape = x.shape[:-3]
516
+ x_flat = x.reshape(-1, 2, self.nlat, mmax).contiguous()
517
+ out = _FusedVectorLegendreForwardFn.apply(x_flat, self.w0_t, self.w1_t)
518
+ return out.reshape(B_shape + (2, self.lmax, mmax))
519
+
520
+ x = torch.view_as_real(x) # [..., 2, nlat, mmax, 2]
521
+
522
+ x00 = x[..., 0, :, :, 0]
523
+ x01 = x[..., 0, :, :, 1]
524
+ x10 = x[..., 1, :, :, 0]
525
+ x11 = x[..., 1, :, :, 1]
526
+
527
+ B_shape = x00.shape[:-2]
528
+ x00_flat = x00.reshape(-1, self.nlat, mmax)
529
+ x01_flat = x01.reshape(-1, self.nlat, mmax)
530
+ x10_flat = x10.reshape(-1, self.nlat, mmax)
531
+ x11_flat = x11.reshape(-1, self.nlat, mmax)
532
+ B = x00_flat.shape[0]
533
+
534
+ if self._use_bf16 and _HAS_CUDA_EXT and x00_flat.is_cuda and not x.requires_grad:
535
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0).bfloat16().contiguous()
536
+ out_w0 = fused_legendre_forward_real(stacked_w0, self.w0_t)
537
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
538
+
539
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0).bfloat16().contiguous()
540
+ out_w1 = fused_legendre_forward_real(stacked_w1, self.w1_t)
541
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
542
+ else:
543
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
544
+ if self._use_bf16:
545
+ stacked_w0 = stacked_w0.bfloat16()
546
+ out_w0 = torch.einsum("bkm,mlk->blm", stacked_w0, self.w0)
547
+ if self._use_bf16:
548
+ out_w0 = out_w0.float()
549
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
550
+
551
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
552
+ if self._use_bf16:
553
+ stacked_w1 = stacked_w1.bfloat16()
554
+ out_w1 = torch.einsum("bkm,mlk->blm", stacked_w1, self.w1)
555
+ if self._use_bf16:
556
+ out_w1 = out_w1.float()
557
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
558
+
559
+ sph_re = r00 - s11
560
+ sph_im = r01 + s10
561
+ tor_re = -s01 - r10
562
+ tor_im = s00 - r11
563
+
564
+ out_shape = list(B_shape) + [2, self.lmax, mmax, 2]
565
+ xout = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
566
+ xout[..., 0, :, :, 0] = sph_re.reshape(B_shape + (self.lmax, mmax))
567
+ xout[..., 0, :, :, 1] = sph_im.reshape(B_shape + (self.lmax, mmax))
568
+ xout[..., 1, :, :, 0] = tor_re.reshape(B_shape + (self.lmax, mmax))
569
+ xout[..., 1, :, :, 1] = tor_im.reshape(B_shape + (self.lmax, mmax))
570
+ return torch.view_as_complex(xout)
571
+
572
+
573
+ class InverseRealVectorSHT(nn.Module):
574
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealVectorSHT``."""
575
+
576
+ def __init__(
577
+ self,
578
+ nlat: int,
579
+ nlon: int,
580
+ lmax: Optional[int] = None,
581
+ mmax: Optional[int] = None,
582
+ grid: str = "equiangular",
583
+ norm: str = "ortho",
584
+ csphase: bool = True,
585
+ ):
586
+ super().__init__()
587
+ from torch_harmonics import InverseRealVectorSHT
588
+ ref = InverseRealVectorSHT(
589
+ nlat,
590
+ nlon,
591
+ lmax=lmax,
592
+ mmax=mmax,
593
+ grid=grid,
594
+ norm=norm,
595
+ csphase=csphase,
596
+ )
597
+ self.nlat = ref.nlat
598
+ self.nlon = ref.nlon
599
+ self.lmax = ref.lmax
600
+ self.mmax = ref.mmax
601
+ self.grid = grid
602
+ self.norm = norm
603
+ self.csphase = csphase
604
+ self.register_buffer("d0", ref.dpct[0].float()) # [mmax, lmax, nlat]
605
+ self.register_buffer("d1", ref.dpct[1].float()) # [mmax, lmax, nlat]
606
+ self.register_buffer("d0_t", ref.dpct[0].float().permute(1, 2, 0).contiguous())
607
+ self.register_buffer("d1_t", ref.dpct[1].float().permute(1, 2, 0).contiguous())
608
+
609
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
610
+ assert x.shape[-2] == self.lmax and x.shape[-1] == self.mmax
611
+
612
+ with _nvtx_range("holysht.vector_inverse"):
613
+ x = x.contiguous()
614
+ if _can_use_cuda_vector(x, self.d0_t, self.d1_t):
615
+ B_shape = x.shape[:-3]
616
+ x_flat = x.reshape(-1, 2, self.lmax, self.mmax).contiguous()
617
+ x_out = _FusedVectorLegendreInverseFn.apply(x_flat, self.d0_t, self.d1_t)
618
+ x_out = x_out.reshape(B_shape + (2, self.nlat, self.mmax))
619
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
620
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
621
+
622
+ x = torch.view_as_real(x) # [..., 2, lmax, mmax, 2]
623
+ mmax = self.mmax
624
+
625
+ x00 = x[..., 0, :, :, 0]
626
+ x01 = x[..., 0, :, :, 1]
627
+ x10 = x[..., 1, :, :, 0]
628
+ x11 = x[..., 1, :, :, 1]
629
+
630
+ B_shape = x00.shape[:-2]
631
+ x00_flat = x00.reshape(-1, self.lmax, mmax)
632
+ x01_flat = x01.reshape(-1, self.lmax, mmax)
633
+ x10_flat = x10.reshape(-1, self.lmax, mmax)
634
+ x11_flat = x11.reshape(-1, self.lmax, mmax)
635
+ B = x00_flat.shape[0]
636
+
637
+ stacked_d0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
638
+ out_d0 = torch.einsum("blm,mlk->bkm", stacked_d0, self.d0)
639
+ r00, r01, r10, r11 = out_d0[:B], out_d0[B:2 * B], out_d0[2 * B:3 * B], out_d0[3 * B:]
640
+
641
+ stacked_d1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
642
+ out_d1 = torch.einsum("blm,mlk->bkm", stacked_d1, self.d1)
643
+ s11, s10, s01, s00 = out_d1[:B], out_d1[B:2 * B], out_d1[2 * B:3 * B], out_d1[3 * B:]
644
+
645
+ srl = r00 - s11
646
+ sim = r01 + s10
647
+ trl = -s01 - r10
648
+ tim = s00 - r11
649
+
650
+ out_k = self.nlat
651
+ srl = srl.reshape(B_shape + (out_k, mmax))
652
+ sim = sim.reshape(B_shape + (out_k, mmax))
653
+ trl = trl.reshape(B_shape + (out_k, mmax))
654
+ tim = tim.reshape(B_shape + (out_k, mmax))
655
+
656
+ s = torch.stack((srl, sim), -1)
657
+ t = torch.stack((trl, tim), -1)
658
+ xs = torch.stack((s, t), -4)
659
+ x_out = torch.view_as_complex(xs)
660
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
661
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
662
+
663
+
664
+ legendre_forward = fused_legendre_forward
665
+ legendre_inverse = fused_legendre_inverse
666
+ sht_forward = fused_sht_forward
667
+ sht_inverse = fused_sht_inverse
668
+
669
+ # Backwards-compatible aliases from the research prototype.
670
+ FusedRealSHT = RealSHT
671
+ FusedInverseRealSHT = InverseRealSHT
672
+ FusedRealVectorSHT = RealVectorSHT
673
+ FusedInverseRealVectorSHT = InverseRealVectorSHT
build/torch210-cxx11-cu126-x86_64-linux/_holysht_cuda_53048d0.abi3.so ADDED
Binary file (132 Bytes). View file
 
build/torch210-cxx11-cu126-x86_64-linux/_jit_ops.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local CUDA extension loader for HOLYSHT.
2
+
3
+ This keeps development off the heavyweight `kernel-builder` path by compiling a
4
+ small torch extension for the current machine only. The compiled artefacts are
5
+ cached under `build/torch_extensions`.
6
+
7
+ Author: Chris von Csefalvay
8
+ Licence: MIT
9
+ Repository: https://github.com/chrisvoncsefalvay/holysht
10
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from torch.utils.cpp_extension import load
20
+
21
+
22
+ _ROOT = Path(__file__).resolve().parents[2]
23
+ _BUILD_DIR = _ROOT / "build" / "torch_extensions"
24
+ _EXTENSION_NAME = "holysht_ops"
25
+
26
+
27
+ def _default_arch_list() -> str:
28
+ if not torch.cuda.is_available():
29
+ return ""
30
+
31
+ major, minor = torch.cuda.get_device_capability(0)
32
+ if (major, minor) >= (12, 1):
33
+ return "12.0+PTX"
34
+ return f"{major}.{minor}"
35
+
36
+
37
+ def _cuda_flags() -> list[str]:
38
+ flags = [
39
+ "-O3",
40
+ "-lineinfo",
41
+ "-Xptxas=-warn-spills",
42
+ "--expt-relaxed-constexpr",
43
+ ]
44
+ if os.environ.get("HOLYSHT_USE_FAST_MATH", "1") != "0":
45
+ flags.append("--use_fast_math")
46
+ return flags
47
+
48
+
49
+ def _load_ops():
50
+ os.environ.setdefault("MAX_JOBS", "1")
51
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _default_arch_list())
52
+ _BUILD_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ load(
55
+ name=_EXTENSION_NAME,
56
+ sources=[
57
+ str(_ROOT / "torch-ext" / "torch_binding.cpp"),
58
+ str(_ROOT / "cuda" / "fused_legendre.cu"),
59
+ str(_ROOT / "cuda" / "fused_sht.cu"),
60
+ ],
61
+ extra_include_paths=[str(_ROOT / "torch-ext")],
62
+ extra_cflags=["-O3", "-std=c++17"],
63
+ extra_cuda_cflags=_cuda_flags(),
64
+ build_directory=str(_BUILD_DIR),
65
+ verbose=os.environ.get("HOLYSHT_VERBOSE_BUILD", "0") == "1",
66
+ with_cuda=True,
67
+ is_python_module=False,
68
+ )
69
+ return getattr(torch.ops, _EXTENSION_NAME)
70
+
71
+
72
+ ops = _load_ops()
build/torch210-cxx11-cu126-x86_64-linux/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _holysht_cuda_53048d0
3
+ ops = torch.ops._holysht_cuda_53048d0
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_holysht_cuda_53048d0::{op_name}"
build/torch210-cxx11-cu126-x86_64-linux/holysht/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch210-cxx11-cu126-x86_64-linux/metadata.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "license": "mit",
4
+ "upstream": "https://github.com/NVIDIA/torch-harmonics",
5
+ "python-depends": [],
6
+ "backend": {
7
+ "type": "cuda"
8
+ }
9
+ }
build/torch210-cxx11-cu128-x86_64-linux/__init__.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HOLYSHT: Highly Optimised Legendre/Ylm/SHT.
2
+
3
+ CUDA-accelerated spherical harmonic transforms designed as a practical,
4
+ production-oriented companion to torch-harmonics.
5
+
6
+ Author: Chris von Csefalvay
7
+ Licence: MIT
8
+ Repository: https://github.com/chrisvoncsefalvay/holysht
9
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
10
+ """
11
+
12
+ import contextlib
13
+ import os
14
+ from typing import Optional
15
+ import torch
16
+ import torch.nn as nn
17
+
18
+ __all__ = [
19
+ "RealSHT",
20
+ "InverseRealSHT",
21
+ "RealVectorSHT",
22
+ "InverseRealVectorSHT",
23
+ "legendre_forward",
24
+ "legendre_inverse",
25
+ "sht_forward",
26
+ "sht_inverse",
27
+ ]
28
+
29
+ # Prefer kernel-builder's generated alias module on packaged builds, then fall
30
+ # back to the local single-machine JIT loader for development.
31
+ try:
32
+ from ._ops import ops as _ops
33
+ _HAS_CUDA_EXT = True
34
+ except ModuleNotFoundError:
35
+ try:
36
+ from ._jit_ops import ops as _ops
37
+ _HAS_CUDA_EXT = True
38
+ except ImportError:
39
+ _HAS_CUDA_EXT = False
40
+ except ImportError:
41
+ _HAS_CUDA_EXT = False
42
+
43
+
44
+ def _can_use_cuda_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
45
+ return (
46
+ _HAS_CUDA_EXT
47
+ and weight_t is not None
48
+ and input.is_cuda
49
+ and weight_t.is_cuda
50
+ and input.dtype == torch.complex64
51
+ and weight_t.dtype == torch.float32
52
+ and weight_t.is_contiguous()
53
+ )
54
+
55
+
56
+ def _can_use_cuda_real_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
57
+ return (
58
+ _HAS_CUDA_EXT
59
+ and weight_t is not None
60
+ and input.is_cuda
61
+ and weight_t.is_cuda
62
+ and input.dtype in (torch.float32, torch.bfloat16)
63
+ and weight_t.dtype == torch.float32
64
+ and input.is_contiguous()
65
+ and weight_t.is_contiguous()
66
+ )
67
+
68
+
69
+ def _can_use_cuda_vector(input: torch.Tensor, weight0_t: Optional[torch.Tensor], weight1_t: Optional[torch.Tensor]) -> bool:
70
+ return (
71
+ _HAS_CUDA_EXT
72
+ and weight0_t is not None
73
+ and weight1_t is not None
74
+ and input.is_cuda
75
+ and weight0_t.is_cuda
76
+ and weight1_t.is_cuda
77
+ and input.dtype == torch.complex64
78
+ and weight0_t.dtype == torch.float32
79
+ and weight1_t.dtype == torch.float32
80
+ and input.is_contiguous()
81
+ and weight0_t.is_contiguous()
82
+ and weight1_t.is_contiguous()
83
+ )
84
+
85
+
86
+ def _mul_i(x: torch.Tensor) -> torch.Tensor:
87
+ """Multiply a complex tensor by +i without promoting dtype."""
88
+ return torch.complex(-x.imag, x.real)
89
+
90
+
91
+ @contextlib.contextmanager
92
+ def _nvtx_range(name: str):
93
+ enabled = os.environ.get("HOLYSHT_ENABLE_NVTX", "0") == "1"
94
+ if enabled and torch.cuda.is_available():
95
+ torch.cuda.nvtx.range_push(name)
96
+ try:
97
+ yield
98
+ finally:
99
+ torch.cuda.nvtx.range_pop()
100
+ else:
101
+ yield
102
+
103
+
104
+ def _prepare_irfft_input(x: torch.Tensor, nlon: int, active_mmax: Optional[int] = None) -> torch.Tensor:
105
+ """Pad/clean an rFFT-format complex tensor before irfft."""
106
+ active_mmax = x.size(-1) if active_mmax is None else active_mmax
107
+ full_mmax = nlon // 2 + 1
108
+
109
+ if x.size(-1) == full_mmax:
110
+ out = x.contiguous()
111
+ else:
112
+ out_shape = list(x.shape)
113
+ out_shape[-1] = full_mmax
114
+ out = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
115
+ out[..., :x.size(-1)] = x
116
+
117
+ if _HAS_CUDA_EXT and out.is_cuda and out.dtype == torch.complex64:
118
+ orig_shape = out.shape
119
+ flat = out.reshape(-1, orig_shape[-2], orig_shape[-1]).contiguous()
120
+ _ops.sht_prepare_irfft(flat, active_mmax, nlon)
121
+ return flat.reshape(orig_shape)
122
+
123
+ out[..., 0] = out[..., 0].real.to(torch.complex64)
124
+ if nlon % 2 == 0:
125
+ nyquist_idx = nlon // 2
126
+ if nyquist_idx < out.size(-1):
127
+ out[..., nyquist_idx] = out[..., nyquist_idx].real.to(torch.complex64)
128
+ return out
129
+
130
+
131
+ class _FusedLegendreForwardFn(torch.autograd.Function):
132
+ @staticmethod
133
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
134
+ input_c = input.contiguous()
135
+ output = torch.empty(
136
+ input_c.size(0), weight_t.size(0), input_c.size(2),
137
+ device=input_c.device, dtype=torch.complex64
138
+ )
139
+ _ops.fused_legendre_forward(output, input_c, weight_t)
140
+ ctx.save_for_backward(weight_t)
141
+ return output
142
+
143
+ @staticmethod
144
+ def backward(ctx, grad_output: torch.Tensor):
145
+ (weight_t,) = ctx.saved_tensors
146
+ grad_output = grad_output.contiguous()
147
+ grad_input = torch.empty(
148
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
149
+ device=grad_output.device, dtype=torch.complex64
150
+ )
151
+ _ops.fused_legendre_inverse(grad_input, grad_output, weight_t)
152
+ return grad_input, None
153
+
154
+
155
+ class _FusedLegendreInverseFn(torch.autograd.Function):
156
+ @staticmethod
157
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
158
+ input_c = input.contiguous()
159
+ output = torch.empty(
160
+ input_c.size(0), weight_t.size(1), input_c.size(2),
161
+ device=input_c.device, dtype=torch.complex64
162
+ )
163
+ _ops.fused_legendre_inverse(output, input_c, weight_t)
164
+ ctx.save_for_backward(weight_t)
165
+ return output
166
+
167
+ @staticmethod
168
+ def backward(ctx, grad_output: torch.Tensor):
169
+ (weight_t,) = ctx.saved_tensors
170
+ grad_output = grad_output.contiguous()
171
+ grad_input = torch.empty(
172
+ grad_output.size(0), weight_t.size(0), grad_output.size(2),
173
+ device=grad_output.device, dtype=torch.complex64
174
+ )
175
+ _ops.fused_legendre_forward(grad_input, grad_output, weight_t)
176
+ return grad_input, None
177
+
178
+
179
+ class _FusedLegendreForwardRealFn(torch.autograd.Function):
180
+ @staticmethod
181
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
182
+ input_c = input.contiguous()
183
+ output = torch.empty(
184
+ input_c.size(0), weight_t.size(0), input_c.size(2),
185
+ device=input_c.device, dtype=torch.float32
186
+ )
187
+ _ops.fused_legendre_forward_real(output, input_c, weight_t)
188
+ ctx.save_for_backward(weight_t)
189
+ return output
190
+
191
+ @staticmethod
192
+ def backward(ctx, grad_output: torch.Tensor):
193
+ (weight_t,) = ctx.saved_tensors
194
+ grad_output = grad_output.contiguous()
195
+ grad_input = torch.empty(
196
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
197
+ device=grad_output.device, dtype=torch.float32
198
+ )
199
+ _ops.fused_legendre_inverse_real(grad_input, grad_output, weight_t)
200
+ return grad_input, None
201
+
202
+
203
+ class _FusedVectorLegendreForwardFn(torch.autograd.Function):
204
+ @staticmethod
205
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
206
+ input_c = input.contiguous()
207
+ output = torch.empty(
208
+ input_c.size(0), 2, weight0_t.size(0), input_c.size(3),
209
+ device=input_c.device, dtype=torch.complex64
210
+ )
211
+ _ops.fused_vector_legendre_forward(output, input_c, weight0_t, weight1_t)
212
+ ctx.save_for_backward(weight0_t, weight1_t)
213
+ return output
214
+
215
+ @staticmethod
216
+ def backward(ctx, grad_output: torch.Tensor):
217
+ weight0_t, weight1_t = ctx.saved_tensors
218
+ grad_output = grad_output.contiguous()
219
+ grad_input = torch.empty(
220
+ grad_output.size(0), 2, weight0_t.size(1), grad_output.size(3),
221
+ device=grad_output.device, dtype=torch.complex64
222
+ )
223
+ _ops.fused_vector_legendre_inverse(grad_input, grad_output, weight0_t, weight1_t)
224
+ return grad_input, None, None
225
+
226
+
227
+ class _FusedVectorLegendreInverseFn(torch.autograd.Function):
228
+ @staticmethod
229
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
230
+ input_c = input.contiguous()
231
+ output = torch.empty(
232
+ input_c.size(0), 2, weight0_t.size(1), input_c.size(3),
233
+ device=input_c.device, dtype=torch.complex64
234
+ )
235
+ _ops.fused_vector_legendre_inverse(output, input_c, weight0_t, weight1_t)
236
+ ctx.save_for_backward(weight0_t, weight1_t)
237
+ return output
238
+
239
+ @staticmethod
240
+ def backward(ctx, grad_output: torch.Tensor):
241
+ weight0_t, weight1_t = ctx.saved_tensors
242
+ grad_output = grad_output.contiguous()
243
+ grad_input = torch.empty(
244
+ grad_output.size(0), 2, weight0_t.size(0), grad_output.size(3),
245
+ device=grad_output.device, dtype=torch.complex64
246
+ )
247
+ _ops.fused_vector_legendre_forward(grad_input, grad_output, weight0_t, weight1_t)
248
+ return grad_input, None, None
249
+
250
+
251
+ # ============================================================================
252
+ # Fused Legendre Transform
253
+ # ============================================================================
254
+
255
+ def fused_legendre_forward(
256
+ input: torch.Tensor, # [B, nlat, mmax] complex64
257
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32 (original torch-harmonics layout)
258
+ weight_t: Optional[torch.Tensor] = None, # [lmax, nlat, mmax] pre-transposed
259
+ ) -> torch.Tensor:
260
+ """Fused forward Legendre transform operating on complex tensors.
261
+
262
+ Computes out[b,l,m] = Σ_k weights[m,l,k] · input[b,k,m] for complex input,
263
+ fusing the real and imaginary multiplications into a single pass.
264
+
265
+ Uses the custom CUDA kernels when the extension is available, otherwise
266
+ falls back to a stacked einsum.
267
+ """
268
+ B = input.size(0)
269
+ nlat = input.size(1)
270
+ mmax = input.size(2)
271
+ lmax = weights.size(1)
272
+
273
+ if weight_t is None:
274
+ weight_t = weights.float().permute(1, 2, 0).contiguous()
275
+
276
+ if _can_use_cuda_legendre(input, weight_t):
277
+ # Adaptive CUDA kernel: small-grid direct path + large-grid tiled path.
278
+ return _FusedLegendreForwardFn.apply(input, weight_t)
279
+ else:
280
+ # Fallback: stacked einsum (1.9x speedup over reference 2x einsum)
281
+ x = torch.view_as_real(input) # [B, nlat, mmax, 2]
282
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0) # [2B, nlat, mmax]
283
+ w = weights.to(x_stacked.dtype)
284
+ out_stacked = torch.einsum("bkm,mlk->blm", x_stacked, w)
285
+ out_re = out_stacked[:B]
286
+ out_im = out_stacked[B:]
287
+ return torch.complex(out_re, out_im)
288
+
289
+
290
+ def fused_legendre_inverse(
291
+ input: torch.Tensor, # [B, lmax, mmax] complex64
292
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
293
+ pct_t: Optional[torch.Tensor] = None,
294
+ ) -> torch.Tensor:
295
+ """Fused inverse Legendre transform."""
296
+ B = input.size(0)
297
+ lmax = input.size(1)
298
+ mmax = input.size(2)
299
+ nlat = pct.size(2)
300
+
301
+ if pct_t is None:
302
+ pct_t = pct.float().permute(1, 2, 0).contiguous()
303
+
304
+ if _can_use_cuda_legendre(input, pct_t):
305
+ return _FusedLegendreInverseFn.apply(input, pct_t)
306
+ else:
307
+ x = torch.view_as_real(input)
308
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0)
309
+ p = pct.to(x_stacked.dtype)
310
+ out_stacked = torch.einsum("blm,mlk->bkm", x_stacked, p)
311
+ return torch.complex(out_stacked[:B], out_stacked[B:])
312
+
313
+
314
+ def fused_legendre_forward_real(
315
+ input: torch.Tensor, # [B, nlat, mmax] float32 or bfloat16
316
+ weight_t: torch.Tensor, # [lmax, nlat, mmax] float32
317
+ ) -> torch.Tensor:
318
+ """Real-valued forward Legendre transform with float accumulation."""
319
+ if _can_use_cuda_real_legendre(input, weight_t):
320
+ return _FusedLegendreForwardRealFn.apply(input, weight_t)
321
+
322
+ return torch.einsum("bkm,lkm->blm", input.float(), weight_t)
323
+
324
+
325
+ # ============================================================================
326
+ # Fused SHT (complete pipeline)
327
+ # ============================================================================
328
+
329
+ def fused_sht_forward(
330
+ x: torch.Tensor, # [B, nlat, nlon] float32
331
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32
332
+ mmax: int,
333
+ weight_t: Optional[torch.Tensor] = None,
334
+ ) -> torch.Tensor:
335
+ """Complete fused forward SHT: rfft → fused Legendre → complex coefficients.
336
+
337
+ Replaces RealSHT.forward() with fewer intermediate allocations.
338
+ """
339
+ with _nvtx_range("holysht.scalar_forward"):
340
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
341
+ x_fft = x_fft[..., :mmax]
342
+ return fused_legendre_forward(x_fft, weights, weight_t)
343
+
344
+
345
+ def fused_sht_inverse(
346
+ coeffs: torch.Tensor, # [B, lmax, mmax] complex64
347
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
348
+ nlon: int,
349
+ pct_t: Optional[torch.Tensor] = None,
350
+ ) -> torch.Tensor:
351
+ """Complete fused inverse SHT: fused Legendre → zero-pad → irfft."""
352
+ with _nvtx_range("holysht.scalar_inverse"):
353
+ x = fused_legendre_inverse(coeffs, pct, pct_t)
354
+ x = _prepare_irfft_input(x, nlon, coeffs.size(-1))
355
+ return torch.fft.irfft(x, n=nlon, dim=-1, norm="forward")
356
+
357
+
358
+ # ============================================================================
359
+ # nn.Module wrappers (drop-in replacements for torch-harmonics)
360
+ # ============================================================================
361
+
362
+ class RealSHT(nn.Module):
363
+ """Optimised drop-in replacement for ``torch_harmonics.RealSHT``.
364
+
365
+ Args:
366
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ nlat: int,
372
+ nlon: int,
373
+ lmax: Optional[int] = None,
374
+ mmax: Optional[int] = None,
375
+ grid: str = "equiangular",
376
+ norm: str = "ortho",
377
+ csphase: bool = True,
378
+ dtype: str = "fp32",
379
+ ):
380
+ super().__init__()
381
+ from torch_harmonics import RealSHT
382
+ ref = RealSHT(
383
+ nlat,
384
+ nlon,
385
+ lmax=lmax,
386
+ mmax=mmax,
387
+ grid=grid,
388
+ norm=norm,
389
+ csphase=csphase,
390
+ )
391
+ self.nlat = ref.nlat
392
+ self.nlon = ref.nlon
393
+ self.lmax = ref.lmax
394
+ self.mmax = ref.mmax
395
+ self.grid = grid
396
+ self.norm = norm
397
+ self.csphase = csphase
398
+ self._use_bf16 = (dtype == "bf16")
399
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
400
+ self.register_buffer("weights", ref.weights.to(w_dtype))
401
+ self.register_buffer("weight_t", ref.weights.float().permute(1, 2, 0).contiguous())
402
+
403
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
404
+ if self._use_bf16:
405
+ with _nvtx_range("holysht.scalar_forward_bf16"):
406
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
407
+ x_fft = x_fft[..., :self.mmax]
408
+ xr = torch.view_as_real(x_fft)
409
+ if _HAS_CUDA_EXT and x.is_cuda and not x.requires_grad:
410
+ xr_bf16 = xr.bfloat16().contiguous()
411
+ out_re = fused_legendre_forward_real(xr_bf16[..., 0].contiguous(), self.weight_t)
412
+ out_im = fused_legendre_forward_real(xr_bf16[..., 1].contiguous(), self.weight_t)
413
+ return torch.complex(out_re, out_im)
414
+
415
+ B = x.size(0)
416
+ xs = torch.cat([xr[..., 0], xr[..., 1]], dim=0).bfloat16()
417
+ out = torch.einsum("bkm,mlk->blm", xs, self.weights).float()
418
+ return torch.complex(out[:B], out[B:])
419
+ return fused_sht_forward(x, self.weights, self.mmax, self.weight_t)
420
+
421
+
422
+ class InverseRealSHT(nn.Module):
423
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealSHT``."""
424
+
425
+ def __init__(
426
+ self,
427
+ nlat: int,
428
+ nlon: int,
429
+ lmax: Optional[int] = None,
430
+ mmax: Optional[int] = None,
431
+ grid: str = "equiangular",
432
+ norm: str = "ortho",
433
+ csphase: bool = True,
434
+ ):
435
+ super().__init__()
436
+ from torch_harmonics import InverseRealSHT
437
+ ref = InverseRealSHT(
438
+ nlat,
439
+ nlon,
440
+ lmax=lmax,
441
+ mmax=mmax,
442
+ grid=grid,
443
+ norm=norm,
444
+ csphase=csphase,
445
+ )
446
+ self.nlat = ref.nlat
447
+ self.nlon = ref.nlon
448
+ self.lmax = ref.lmax
449
+ self.mmax = ref.mmax
450
+ self.grid = grid
451
+ self.norm = norm
452
+ self.csphase = csphase
453
+ self.register_buffer("pct", ref.pct.float())
454
+ self.register_buffer("pct_t", ref.pct.float().permute(1, 2, 0).contiguous())
455
+
456
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
457
+ return fused_sht_inverse(x, self.pct, self.nlon, self.pct_t)
458
+
459
+
460
+ class RealVectorSHT(nn.Module):
461
+ """Optimised drop-in replacement for ``torch_harmonics.RealVectorSHT``.
462
+
463
+ Reduces eight reference einsums to two composed Legendre passes on the
464
+ default FP32 CUDA path.
465
+
466
+ Args:
467
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
468
+ """
469
+
470
+ def __init__(
471
+ self,
472
+ nlat: int,
473
+ nlon: int,
474
+ lmax: Optional[int] = None,
475
+ mmax: Optional[int] = None,
476
+ grid: str = "equiangular",
477
+ norm: str = "ortho",
478
+ csphase: bool = True,
479
+ dtype: str = "fp32",
480
+ ):
481
+ super().__init__()
482
+ from torch_harmonics import RealVectorSHT
483
+ ref = RealVectorSHT(
484
+ nlat,
485
+ nlon,
486
+ lmax=lmax,
487
+ mmax=mmax,
488
+ grid=grid,
489
+ norm=norm,
490
+ csphase=csphase,
491
+ )
492
+ self.nlat = ref.nlat
493
+ self.nlon = ref.nlon
494
+ self.lmax = ref.lmax
495
+ self.mmax = ref.mmax
496
+ self.grid = grid
497
+ self.norm = norm
498
+ self.csphase = csphase
499
+ self._use_bf16 = (dtype == "bf16")
500
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
501
+ self.register_buffer("w0", ref.weights[0].to(w_dtype)) # [mmax, lmax, nlat]
502
+ self.register_buffer("w1", ref.weights[1].to(w_dtype)) # [mmax, lmax, nlat]
503
+ self.register_buffer("w0_t", ref.weights[0].float().permute(1, 2, 0).contiguous())
504
+ self.register_buffer("w1_t", ref.weights[1].float().permute(1, 2, 0).contiguous())
505
+
506
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
507
+ assert x.shape[-2] == self.nlat and x.shape[-1] == self.nlon
508
+
509
+ with _nvtx_range("holysht.vector_forward"):
510
+ x = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
511
+ mmax = self.mmax
512
+ x = x[..., :mmax].contiguous()
513
+
514
+ if (not self._use_bf16) and _can_use_cuda_vector(x, self.w0_t, self.w1_t):
515
+ B_shape = x.shape[:-3]
516
+ x_flat = x.reshape(-1, 2, self.nlat, mmax).contiguous()
517
+ out = _FusedVectorLegendreForwardFn.apply(x_flat, self.w0_t, self.w1_t)
518
+ return out.reshape(B_shape + (2, self.lmax, mmax))
519
+
520
+ x = torch.view_as_real(x) # [..., 2, nlat, mmax, 2]
521
+
522
+ x00 = x[..., 0, :, :, 0]
523
+ x01 = x[..., 0, :, :, 1]
524
+ x10 = x[..., 1, :, :, 0]
525
+ x11 = x[..., 1, :, :, 1]
526
+
527
+ B_shape = x00.shape[:-2]
528
+ x00_flat = x00.reshape(-1, self.nlat, mmax)
529
+ x01_flat = x01.reshape(-1, self.nlat, mmax)
530
+ x10_flat = x10.reshape(-1, self.nlat, mmax)
531
+ x11_flat = x11.reshape(-1, self.nlat, mmax)
532
+ B = x00_flat.shape[0]
533
+
534
+ if self._use_bf16 and _HAS_CUDA_EXT and x00_flat.is_cuda and not x.requires_grad:
535
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0).bfloat16().contiguous()
536
+ out_w0 = fused_legendre_forward_real(stacked_w0, self.w0_t)
537
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
538
+
539
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0).bfloat16().contiguous()
540
+ out_w1 = fused_legendre_forward_real(stacked_w1, self.w1_t)
541
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
542
+ else:
543
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
544
+ if self._use_bf16:
545
+ stacked_w0 = stacked_w0.bfloat16()
546
+ out_w0 = torch.einsum("bkm,mlk->blm", stacked_w0, self.w0)
547
+ if self._use_bf16:
548
+ out_w0 = out_w0.float()
549
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
550
+
551
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
552
+ if self._use_bf16:
553
+ stacked_w1 = stacked_w1.bfloat16()
554
+ out_w1 = torch.einsum("bkm,mlk->blm", stacked_w1, self.w1)
555
+ if self._use_bf16:
556
+ out_w1 = out_w1.float()
557
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
558
+
559
+ sph_re = r00 - s11
560
+ sph_im = r01 + s10
561
+ tor_re = -s01 - r10
562
+ tor_im = s00 - r11
563
+
564
+ out_shape = list(B_shape) + [2, self.lmax, mmax, 2]
565
+ xout = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
566
+ xout[..., 0, :, :, 0] = sph_re.reshape(B_shape + (self.lmax, mmax))
567
+ xout[..., 0, :, :, 1] = sph_im.reshape(B_shape + (self.lmax, mmax))
568
+ xout[..., 1, :, :, 0] = tor_re.reshape(B_shape + (self.lmax, mmax))
569
+ xout[..., 1, :, :, 1] = tor_im.reshape(B_shape + (self.lmax, mmax))
570
+ return torch.view_as_complex(xout)
571
+
572
+
573
+ class InverseRealVectorSHT(nn.Module):
574
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealVectorSHT``."""
575
+
576
+ def __init__(
577
+ self,
578
+ nlat: int,
579
+ nlon: int,
580
+ lmax: Optional[int] = None,
581
+ mmax: Optional[int] = None,
582
+ grid: str = "equiangular",
583
+ norm: str = "ortho",
584
+ csphase: bool = True,
585
+ ):
586
+ super().__init__()
587
+ from torch_harmonics import InverseRealVectorSHT
588
+ ref = InverseRealVectorSHT(
589
+ nlat,
590
+ nlon,
591
+ lmax=lmax,
592
+ mmax=mmax,
593
+ grid=grid,
594
+ norm=norm,
595
+ csphase=csphase,
596
+ )
597
+ self.nlat = ref.nlat
598
+ self.nlon = ref.nlon
599
+ self.lmax = ref.lmax
600
+ self.mmax = ref.mmax
601
+ self.grid = grid
602
+ self.norm = norm
603
+ self.csphase = csphase
604
+ self.register_buffer("d0", ref.dpct[0].float()) # [mmax, lmax, nlat]
605
+ self.register_buffer("d1", ref.dpct[1].float()) # [mmax, lmax, nlat]
606
+ self.register_buffer("d0_t", ref.dpct[0].float().permute(1, 2, 0).contiguous())
607
+ self.register_buffer("d1_t", ref.dpct[1].float().permute(1, 2, 0).contiguous())
608
+
609
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
610
+ assert x.shape[-2] == self.lmax and x.shape[-1] == self.mmax
611
+
612
+ with _nvtx_range("holysht.vector_inverse"):
613
+ x = x.contiguous()
614
+ if _can_use_cuda_vector(x, self.d0_t, self.d1_t):
615
+ B_shape = x.shape[:-3]
616
+ x_flat = x.reshape(-1, 2, self.lmax, self.mmax).contiguous()
617
+ x_out = _FusedVectorLegendreInverseFn.apply(x_flat, self.d0_t, self.d1_t)
618
+ x_out = x_out.reshape(B_shape + (2, self.nlat, self.mmax))
619
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
620
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
621
+
622
+ x = torch.view_as_real(x) # [..., 2, lmax, mmax, 2]
623
+ mmax = self.mmax
624
+
625
+ x00 = x[..., 0, :, :, 0]
626
+ x01 = x[..., 0, :, :, 1]
627
+ x10 = x[..., 1, :, :, 0]
628
+ x11 = x[..., 1, :, :, 1]
629
+
630
+ B_shape = x00.shape[:-2]
631
+ x00_flat = x00.reshape(-1, self.lmax, mmax)
632
+ x01_flat = x01.reshape(-1, self.lmax, mmax)
633
+ x10_flat = x10.reshape(-1, self.lmax, mmax)
634
+ x11_flat = x11.reshape(-1, self.lmax, mmax)
635
+ B = x00_flat.shape[0]
636
+
637
+ stacked_d0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
638
+ out_d0 = torch.einsum("blm,mlk->bkm", stacked_d0, self.d0)
639
+ r00, r01, r10, r11 = out_d0[:B], out_d0[B:2 * B], out_d0[2 * B:3 * B], out_d0[3 * B:]
640
+
641
+ stacked_d1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
642
+ out_d1 = torch.einsum("blm,mlk->bkm", stacked_d1, self.d1)
643
+ s11, s10, s01, s00 = out_d1[:B], out_d1[B:2 * B], out_d1[2 * B:3 * B], out_d1[3 * B:]
644
+
645
+ srl = r00 - s11
646
+ sim = r01 + s10
647
+ trl = -s01 - r10
648
+ tim = s00 - r11
649
+
650
+ out_k = self.nlat
651
+ srl = srl.reshape(B_shape + (out_k, mmax))
652
+ sim = sim.reshape(B_shape + (out_k, mmax))
653
+ trl = trl.reshape(B_shape + (out_k, mmax))
654
+ tim = tim.reshape(B_shape + (out_k, mmax))
655
+
656
+ s = torch.stack((srl, sim), -1)
657
+ t = torch.stack((trl, tim), -1)
658
+ xs = torch.stack((s, t), -4)
659
+ x_out = torch.view_as_complex(xs)
660
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
661
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
662
+
663
+
664
+ legendre_forward = fused_legendre_forward
665
+ legendre_inverse = fused_legendre_inverse
666
+ sht_forward = fused_sht_forward
667
+ sht_inverse = fused_sht_inverse
668
+
669
+ # Backwards-compatible aliases from the research prototype.
670
+ FusedRealSHT = RealSHT
671
+ FusedInverseRealSHT = InverseRealSHT
672
+ FusedRealVectorSHT = RealVectorSHT
673
+ FusedInverseRealVectorSHT = InverseRealVectorSHT
build/torch210-cxx11-cu128-x86_64-linux/_holysht_cuda_53048d0.abi3.so ADDED
Binary file (132 Bytes). View file
 
build/torch210-cxx11-cu128-x86_64-linux/_jit_ops.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local CUDA extension loader for HOLYSHT.
2
+
3
+ This keeps development off the heavyweight `kernel-builder` path by compiling a
4
+ small torch extension for the current machine only. The compiled artefacts are
5
+ cached under `build/torch_extensions`.
6
+
7
+ Author: Chris von Csefalvay
8
+ Licence: MIT
9
+ Repository: https://github.com/chrisvoncsefalvay/holysht
10
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from torch.utils.cpp_extension import load
20
+
21
+
22
+ _ROOT = Path(__file__).resolve().parents[2]
23
+ _BUILD_DIR = _ROOT / "build" / "torch_extensions"
24
+ _EXTENSION_NAME = "holysht_ops"
25
+
26
+
27
+ def _default_arch_list() -> str:
28
+ if not torch.cuda.is_available():
29
+ return ""
30
+
31
+ major, minor = torch.cuda.get_device_capability(0)
32
+ if (major, minor) >= (12, 1):
33
+ return "12.0+PTX"
34
+ return f"{major}.{minor}"
35
+
36
+
37
+ def _cuda_flags() -> list[str]:
38
+ flags = [
39
+ "-O3",
40
+ "-lineinfo",
41
+ "-Xptxas=-warn-spills",
42
+ "--expt-relaxed-constexpr",
43
+ ]
44
+ if os.environ.get("HOLYSHT_USE_FAST_MATH", "1") != "0":
45
+ flags.append("--use_fast_math")
46
+ return flags
47
+
48
+
49
+ def _load_ops():
50
+ os.environ.setdefault("MAX_JOBS", "1")
51
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _default_arch_list())
52
+ _BUILD_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ load(
55
+ name=_EXTENSION_NAME,
56
+ sources=[
57
+ str(_ROOT / "torch-ext" / "torch_binding.cpp"),
58
+ str(_ROOT / "cuda" / "fused_legendre.cu"),
59
+ str(_ROOT / "cuda" / "fused_sht.cu"),
60
+ ],
61
+ extra_include_paths=[str(_ROOT / "torch-ext")],
62
+ extra_cflags=["-O3", "-std=c++17"],
63
+ extra_cuda_cflags=_cuda_flags(),
64
+ build_directory=str(_BUILD_DIR),
65
+ verbose=os.environ.get("HOLYSHT_VERBOSE_BUILD", "0") == "1",
66
+ with_cuda=True,
67
+ is_python_module=False,
68
+ )
69
+ return getattr(torch.ops, _EXTENSION_NAME)
70
+
71
+
72
+ ops = _load_ops()
build/torch210-cxx11-cu128-x86_64-linux/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _holysht_cuda_53048d0
3
+ ops = torch.ops._holysht_cuda_53048d0
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_holysht_cuda_53048d0::{op_name}"
build/torch210-cxx11-cu128-x86_64-linux/holysht/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch210-cxx11-cu128-x86_64-linux/metadata.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "license": "mit",
4
+ "upstream": "https://github.com/NVIDIA/torch-harmonics",
5
+ "python-depends": [],
6
+ "backend": {
7
+ "type": "cuda",
8
+ "archs": [
9
+ "12.0"
10
+ ]
11
+ }
12
+ }
build/torch210-cxx11-cu130-x86_64-linux/__init__.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HOLYSHT: Highly Optimised Legendre/Ylm/SHT.
2
+
3
+ CUDA-accelerated spherical harmonic transforms designed as a practical,
4
+ production-oriented companion to torch-harmonics.
5
+
6
+ Author: Chris von Csefalvay
7
+ Licence: MIT
8
+ Repository: https://github.com/chrisvoncsefalvay/holysht
9
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
10
+ """
11
+
12
+ import contextlib
13
+ import os
14
+ from typing import Optional
15
+ import torch
16
+ import torch.nn as nn
17
+
18
+ __all__ = [
19
+ "RealSHT",
20
+ "InverseRealSHT",
21
+ "RealVectorSHT",
22
+ "InverseRealVectorSHT",
23
+ "legendre_forward",
24
+ "legendre_inverse",
25
+ "sht_forward",
26
+ "sht_inverse",
27
+ ]
28
+
29
+ # Prefer kernel-builder's generated alias module on packaged builds, then fall
30
+ # back to the local single-machine JIT loader for development.
31
+ try:
32
+ from ._ops import ops as _ops
33
+ _HAS_CUDA_EXT = True
34
+ except ModuleNotFoundError:
35
+ try:
36
+ from ._jit_ops import ops as _ops
37
+ _HAS_CUDA_EXT = True
38
+ except ImportError:
39
+ _HAS_CUDA_EXT = False
40
+ except ImportError:
41
+ _HAS_CUDA_EXT = False
42
+
43
+
44
+ def _can_use_cuda_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
45
+ return (
46
+ _HAS_CUDA_EXT
47
+ and weight_t is not None
48
+ and input.is_cuda
49
+ and weight_t.is_cuda
50
+ and input.dtype == torch.complex64
51
+ and weight_t.dtype == torch.float32
52
+ and weight_t.is_contiguous()
53
+ )
54
+
55
+
56
+ def _can_use_cuda_real_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
57
+ return (
58
+ _HAS_CUDA_EXT
59
+ and weight_t is not None
60
+ and input.is_cuda
61
+ and weight_t.is_cuda
62
+ and input.dtype in (torch.float32, torch.bfloat16)
63
+ and weight_t.dtype == torch.float32
64
+ and input.is_contiguous()
65
+ and weight_t.is_contiguous()
66
+ )
67
+
68
+
69
+ def _can_use_cuda_vector(input: torch.Tensor, weight0_t: Optional[torch.Tensor], weight1_t: Optional[torch.Tensor]) -> bool:
70
+ return (
71
+ _HAS_CUDA_EXT
72
+ and weight0_t is not None
73
+ and weight1_t is not None
74
+ and input.is_cuda
75
+ and weight0_t.is_cuda
76
+ and weight1_t.is_cuda
77
+ and input.dtype == torch.complex64
78
+ and weight0_t.dtype == torch.float32
79
+ and weight1_t.dtype == torch.float32
80
+ and input.is_contiguous()
81
+ and weight0_t.is_contiguous()
82
+ and weight1_t.is_contiguous()
83
+ )
84
+
85
+
86
+ def _mul_i(x: torch.Tensor) -> torch.Tensor:
87
+ """Multiply a complex tensor by +i without promoting dtype."""
88
+ return torch.complex(-x.imag, x.real)
89
+
90
+
91
+ @contextlib.contextmanager
92
+ def _nvtx_range(name: str):
93
+ enabled = os.environ.get("HOLYSHT_ENABLE_NVTX", "0") == "1"
94
+ if enabled and torch.cuda.is_available():
95
+ torch.cuda.nvtx.range_push(name)
96
+ try:
97
+ yield
98
+ finally:
99
+ torch.cuda.nvtx.range_pop()
100
+ else:
101
+ yield
102
+
103
+
104
+ def _prepare_irfft_input(x: torch.Tensor, nlon: int, active_mmax: Optional[int] = None) -> torch.Tensor:
105
+ """Pad/clean an rFFT-format complex tensor before irfft."""
106
+ active_mmax = x.size(-1) if active_mmax is None else active_mmax
107
+ full_mmax = nlon // 2 + 1
108
+
109
+ if x.size(-1) == full_mmax:
110
+ out = x.contiguous()
111
+ else:
112
+ out_shape = list(x.shape)
113
+ out_shape[-1] = full_mmax
114
+ out = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
115
+ out[..., :x.size(-1)] = x
116
+
117
+ if _HAS_CUDA_EXT and out.is_cuda and out.dtype == torch.complex64:
118
+ orig_shape = out.shape
119
+ flat = out.reshape(-1, orig_shape[-2], orig_shape[-1]).contiguous()
120
+ _ops.sht_prepare_irfft(flat, active_mmax, nlon)
121
+ return flat.reshape(orig_shape)
122
+
123
+ out[..., 0] = out[..., 0].real.to(torch.complex64)
124
+ if nlon % 2 == 0:
125
+ nyquist_idx = nlon // 2
126
+ if nyquist_idx < out.size(-1):
127
+ out[..., nyquist_idx] = out[..., nyquist_idx].real.to(torch.complex64)
128
+ return out
129
+
130
+
131
+ class _FusedLegendreForwardFn(torch.autograd.Function):
132
+ @staticmethod
133
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
134
+ input_c = input.contiguous()
135
+ output = torch.empty(
136
+ input_c.size(0), weight_t.size(0), input_c.size(2),
137
+ device=input_c.device, dtype=torch.complex64
138
+ )
139
+ _ops.fused_legendre_forward(output, input_c, weight_t)
140
+ ctx.save_for_backward(weight_t)
141
+ return output
142
+
143
+ @staticmethod
144
+ def backward(ctx, grad_output: torch.Tensor):
145
+ (weight_t,) = ctx.saved_tensors
146
+ grad_output = grad_output.contiguous()
147
+ grad_input = torch.empty(
148
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
149
+ device=grad_output.device, dtype=torch.complex64
150
+ )
151
+ _ops.fused_legendre_inverse(grad_input, grad_output, weight_t)
152
+ return grad_input, None
153
+
154
+
155
+ class _FusedLegendreInverseFn(torch.autograd.Function):
156
+ @staticmethod
157
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
158
+ input_c = input.contiguous()
159
+ output = torch.empty(
160
+ input_c.size(0), weight_t.size(1), input_c.size(2),
161
+ device=input_c.device, dtype=torch.complex64
162
+ )
163
+ _ops.fused_legendre_inverse(output, input_c, weight_t)
164
+ ctx.save_for_backward(weight_t)
165
+ return output
166
+
167
+ @staticmethod
168
+ def backward(ctx, grad_output: torch.Tensor):
169
+ (weight_t,) = ctx.saved_tensors
170
+ grad_output = grad_output.contiguous()
171
+ grad_input = torch.empty(
172
+ grad_output.size(0), weight_t.size(0), grad_output.size(2),
173
+ device=grad_output.device, dtype=torch.complex64
174
+ )
175
+ _ops.fused_legendre_forward(grad_input, grad_output, weight_t)
176
+ return grad_input, None
177
+
178
+
179
+ class _FusedLegendreForwardRealFn(torch.autograd.Function):
180
+ @staticmethod
181
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
182
+ input_c = input.contiguous()
183
+ output = torch.empty(
184
+ input_c.size(0), weight_t.size(0), input_c.size(2),
185
+ device=input_c.device, dtype=torch.float32
186
+ )
187
+ _ops.fused_legendre_forward_real(output, input_c, weight_t)
188
+ ctx.save_for_backward(weight_t)
189
+ return output
190
+
191
+ @staticmethod
192
+ def backward(ctx, grad_output: torch.Tensor):
193
+ (weight_t,) = ctx.saved_tensors
194
+ grad_output = grad_output.contiguous()
195
+ grad_input = torch.empty(
196
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
197
+ device=grad_output.device, dtype=torch.float32
198
+ )
199
+ _ops.fused_legendre_inverse_real(grad_input, grad_output, weight_t)
200
+ return grad_input, None
201
+
202
+
203
+ class _FusedVectorLegendreForwardFn(torch.autograd.Function):
204
+ @staticmethod
205
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
206
+ input_c = input.contiguous()
207
+ output = torch.empty(
208
+ input_c.size(0), 2, weight0_t.size(0), input_c.size(3),
209
+ device=input_c.device, dtype=torch.complex64
210
+ )
211
+ _ops.fused_vector_legendre_forward(output, input_c, weight0_t, weight1_t)
212
+ ctx.save_for_backward(weight0_t, weight1_t)
213
+ return output
214
+
215
+ @staticmethod
216
+ def backward(ctx, grad_output: torch.Tensor):
217
+ weight0_t, weight1_t = ctx.saved_tensors
218
+ grad_output = grad_output.contiguous()
219
+ grad_input = torch.empty(
220
+ grad_output.size(0), 2, weight0_t.size(1), grad_output.size(3),
221
+ device=grad_output.device, dtype=torch.complex64
222
+ )
223
+ _ops.fused_vector_legendre_inverse(grad_input, grad_output, weight0_t, weight1_t)
224
+ return grad_input, None, None
225
+
226
+
227
+ class _FusedVectorLegendreInverseFn(torch.autograd.Function):
228
+ @staticmethod
229
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
230
+ input_c = input.contiguous()
231
+ output = torch.empty(
232
+ input_c.size(0), 2, weight0_t.size(1), input_c.size(3),
233
+ device=input_c.device, dtype=torch.complex64
234
+ )
235
+ _ops.fused_vector_legendre_inverse(output, input_c, weight0_t, weight1_t)
236
+ ctx.save_for_backward(weight0_t, weight1_t)
237
+ return output
238
+
239
+ @staticmethod
240
+ def backward(ctx, grad_output: torch.Tensor):
241
+ weight0_t, weight1_t = ctx.saved_tensors
242
+ grad_output = grad_output.contiguous()
243
+ grad_input = torch.empty(
244
+ grad_output.size(0), 2, weight0_t.size(0), grad_output.size(3),
245
+ device=grad_output.device, dtype=torch.complex64
246
+ )
247
+ _ops.fused_vector_legendre_forward(grad_input, grad_output, weight0_t, weight1_t)
248
+ return grad_input, None, None
249
+
250
+
251
+ # ============================================================================
252
+ # Fused Legendre Transform
253
+ # ============================================================================
254
+
255
+ def fused_legendre_forward(
256
+ input: torch.Tensor, # [B, nlat, mmax] complex64
257
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32 (original torch-harmonics layout)
258
+ weight_t: Optional[torch.Tensor] = None, # [lmax, nlat, mmax] pre-transposed
259
+ ) -> torch.Tensor:
260
+ """Fused forward Legendre transform operating on complex tensors.
261
+
262
+ Computes out[b,l,m] = Σ_k weights[m,l,k] · input[b,k,m] for complex input,
263
+ fusing the real and imaginary multiplications into a single pass.
264
+
265
+ Uses the custom CUDA kernels when the extension is available, otherwise
266
+ falls back to a stacked einsum.
267
+ """
268
+ B = input.size(0)
269
+ nlat = input.size(1)
270
+ mmax = input.size(2)
271
+ lmax = weights.size(1)
272
+
273
+ if weight_t is None:
274
+ weight_t = weights.float().permute(1, 2, 0).contiguous()
275
+
276
+ if _can_use_cuda_legendre(input, weight_t):
277
+ # Adaptive CUDA kernel: small-grid direct path + large-grid tiled path.
278
+ return _FusedLegendreForwardFn.apply(input, weight_t)
279
+ else:
280
+ # Fallback: stacked einsum (1.9x speedup over reference 2x einsum)
281
+ x = torch.view_as_real(input) # [B, nlat, mmax, 2]
282
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0) # [2B, nlat, mmax]
283
+ w = weights.to(x_stacked.dtype)
284
+ out_stacked = torch.einsum("bkm,mlk->blm", x_stacked, w)
285
+ out_re = out_stacked[:B]
286
+ out_im = out_stacked[B:]
287
+ return torch.complex(out_re, out_im)
288
+
289
+
290
+ def fused_legendre_inverse(
291
+ input: torch.Tensor, # [B, lmax, mmax] complex64
292
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
293
+ pct_t: Optional[torch.Tensor] = None,
294
+ ) -> torch.Tensor:
295
+ """Fused inverse Legendre transform."""
296
+ B = input.size(0)
297
+ lmax = input.size(1)
298
+ mmax = input.size(2)
299
+ nlat = pct.size(2)
300
+
301
+ if pct_t is None:
302
+ pct_t = pct.float().permute(1, 2, 0).contiguous()
303
+
304
+ if _can_use_cuda_legendre(input, pct_t):
305
+ return _FusedLegendreInverseFn.apply(input, pct_t)
306
+ else:
307
+ x = torch.view_as_real(input)
308
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0)
309
+ p = pct.to(x_stacked.dtype)
310
+ out_stacked = torch.einsum("blm,mlk->bkm", x_stacked, p)
311
+ return torch.complex(out_stacked[:B], out_stacked[B:])
312
+
313
+
314
+ def fused_legendre_forward_real(
315
+ input: torch.Tensor, # [B, nlat, mmax] float32 or bfloat16
316
+ weight_t: torch.Tensor, # [lmax, nlat, mmax] float32
317
+ ) -> torch.Tensor:
318
+ """Real-valued forward Legendre transform with float accumulation."""
319
+ if _can_use_cuda_real_legendre(input, weight_t):
320
+ return _FusedLegendreForwardRealFn.apply(input, weight_t)
321
+
322
+ return torch.einsum("bkm,lkm->blm", input.float(), weight_t)
323
+
324
+
325
+ # ============================================================================
326
+ # Fused SHT (complete pipeline)
327
+ # ============================================================================
328
+
329
+ def fused_sht_forward(
330
+ x: torch.Tensor, # [B, nlat, nlon] float32
331
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32
332
+ mmax: int,
333
+ weight_t: Optional[torch.Tensor] = None,
334
+ ) -> torch.Tensor:
335
+ """Complete fused forward SHT: rfft → fused Legendre → complex coefficients.
336
+
337
+ Replaces RealSHT.forward() with fewer intermediate allocations.
338
+ """
339
+ with _nvtx_range("holysht.scalar_forward"):
340
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
341
+ x_fft = x_fft[..., :mmax]
342
+ return fused_legendre_forward(x_fft, weights, weight_t)
343
+
344
+
345
+ def fused_sht_inverse(
346
+ coeffs: torch.Tensor, # [B, lmax, mmax] complex64
347
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
348
+ nlon: int,
349
+ pct_t: Optional[torch.Tensor] = None,
350
+ ) -> torch.Tensor:
351
+ """Complete fused inverse SHT: fused Legendre → zero-pad → irfft."""
352
+ with _nvtx_range("holysht.scalar_inverse"):
353
+ x = fused_legendre_inverse(coeffs, pct, pct_t)
354
+ x = _prepare_irfft_input(x, nlon, coeffs.size(-1))
355
+ return torch.fft.irfft(x, n=nlon, dim=-1, norm="forward")
356
+
357
+
358
+ # ============================================================================
359
+ # nn.Module wrappers (drop-in replacements for torch-harmonics)
360
+ # ============================================================================
361
+
362
+ class RealSHT(nn.Module):
363
+ """Optimised drop-in replacement for ``torch_harmonics.RealSHT``.
364
+
365
+ Args:
366
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ nlat: int,
372
+ nlon: int,
373
+ lmax: Optional[int] = None,
374
+ mmax: Optional[int] = None,
375
+ grid: str = "equiangular",
376
+ norm: str = "ortho",
377
+ csphase: bool = True,
378
+ dtype: str = "fp32",
379
+ ):
380
+ super().__init__()
381
+ from torch_harmonics import RealSHT
382
+ ref = RealSHT(
383
+ nlat,
384
+ nlon,
385
+ lmax=lmax,
386
+ mmax=mmax,
387
+ grid=grid,
388
+ norm=norm,
389
+ csphase=csphase,
390
+ )
391
+ self.nlat = ref.nlat
392
+ self.nlon = ref.nlon
393
+ self.lmax = ref.lmax
394
+ self.mmax = ref.mmax
395
+ self.grid = grid
396
+ self.norm = norm
397
+ self.csphase = csphase
398
+ self._use_bf16 = (dtype == "bf16")
399
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
400
+ self.register_buffer("weights", ref.weights.to(w_dtype))
401
+ self.register_buffer("weight_t", ref.weights.float().permute(1, 2, 0).contiguous())
402
+
403
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
404
+ if self._use_bf16:
405
+ with _nvtx_range("holysht.scalar_forward_bf16"):
406
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
407
+ x_fft = x_fft[..., :self.mmax]
408
+ xr = torch.view_as_real(x_fft)
409
+ if _HAS_CUDA_EXT and x.is_cuda and not x.requires_grad:
410
+ xr_bf16 = xr.bfloat16().contiguous()
411
+ out_re = fused_legendre_forward_real(xr_bf16[..., 0].contiguous(), self.weight_t)
412
+ out_im = fused_legendre_forward_real(xr_bf16[..., 1].contiguous(), self.weight_t)
413
+ return torch.complex(out_re, out_im)
414
+
415
+ B = x.size(0)
416
+ xs = torch.cat([xr[..., 0], xr[..., 1]], dim=0).bfloat16()
417
+ out = torch.einsum("bkm,mlk->blm", xs, self.weights).float()
418
+ return torch.complex(out[:B], out[B:])
419
+ return fused_sht_forward(x, self.weights, self.mmax, self.weight_t)
420
+
421
+
422
+ class InverseRealSHT(nn.Module):
423
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealSHT``."""
424
+
425
+ def __init__(
426
+ self,
427
+ nlat: int,
428
+ nlon: int,
429
+ lmax: Optional[int] = None,
430
+ mmax: Optional[int] = None,
431
+ grid: str = "equiangular",
432
+ norm: str = "ortho",
433
+ csphase: bool = True,
434
+ ):
435
+ super().__init__()
436
+ from torch_harmonics import InverseRealSHT
437
+ ref = InverseRealSHT(
438
+ nlat,
439
+ nlon,
440
+ lmax=lmax,
441
+ mmax=mmax,
442
+ grid=grid,
443
+ norm=norm,
444
+ csphase=csphase,
445
+ )
446
+ self.nlat = ref.nlat
447
+ self.nlon = ref.nlon
448
+ self.lmax = ref.lmax
449
+ self.mmax = ref.mmax
450
+ self.grid = grid
451
+ self.norm = norm
452
+ self.csphase = csphase
453
+ self.register_buffer("pct", ref.pct.float())
454
+ self.register_buffer("pct_t", ref.pct.float().permute(1, 2, 0).contiguous())
455
+
456
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
457
+ return fused_sht_inverse(x, self.pct, self.nlon, self.pct_t)
458
+
459
+
460
+ class RealVectorSHT(nn.Module):
461
+ """Optimised drop-in replacement for ``torch_harmonics.RealVectorSHT``.
462
+
463
+ Reduces eight reference einsums to two composed Legendre passes on the
464
+ default FP32 CUDA path.
465
+
466
+ Args:
467
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
468
+ """
469
+
470
+ def __init__(
471
+ self,
472
+ nlat: int,
473
+ nlon: int,
474
+ lmax: Optional[int] = None,
475
+ mmax: Optional[int] = None,
476
+ grid: str = "equiangular",
477
+ norm: str = "ortho",
478
+ csphase: bool = True,
479
+ dtype: str = "fp32",
480
+ ):
481
+ super().__init__()
482
+ from torch_harmonics import RealVectorSHT
483
+ ref = RealVectorSHT(
484
+ nlat,
485
+ nlon,
486
+ lmax=lmax,
487
+ mmax=mmax,
488
+ grid=grid,
489
+ norm=norm,
490
+ csphase=csphase,
491
+ )
492
+ self.nlat = ref.nlat
493
+ self.nlon = ref.nlon
494
+ self.lmax = ref.lmax
495
+ self.mmax = ref.mmax
496
+ self.grid = grid
497
+ self.norm = norm
498
+ self.csphase = csphase
499
+ self._use_bf16 = (dtype == "bf16")
500
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
501
+ self.register_buffer("w0", ref.weights[0].to(w_dtype)) # [mmax, lmax, nlat]
502
+ self.register_buffer("w1", ref.weights[1].to(w_dtype)) # [mmax, lmax, nlat]
503
+ self.register_buffer("w0_t", ref.weights[0].float().permute(1, 2, 0).contiguous())
504
+ self.register_buffer("w1_t", ref.weights[1].float().permute(1, 2, 0).contiguous())
505
+
506
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
507
+ assert x.shape[-2] == self.nlat and x.shape[-1] == self.nlon
508
+
509
+ with _nvtx_range("holysht.vector_forward"):
510
+ x = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
511
+ mmax = self.mmax
512
+ x = x[..., :mmax].contiguous()
513
+
514
+ if (not self._use_bf16) and _can_use_cuda_vector(x, self.w0_t, self.w1_t):
515
+ B_shape = x.shape[:-3]
516
+ x_flat = x.reshape(-1, 2, self.nlat, mmax).contiguous()
517
+ out = _FusedVectorLegendreForwardFn.apply(x_flat, self.w0_t, self.w1_t)
518
+ return out.reshape(B_shape + (2, self.lmax, mmax))
519
+
520
+ x = torch.view_as_real(x) # [..., 2, nlat, mmax, 2]
521
+
522
+ x00 = x[..., 0, :, :, 0]
523
+ x01 = x[..., 0, :, :, 1]
524
+ x10 = x[..., 1, :, :, 0]
525
+ x11 = x[..., 1, :, :, 1]
526
+
527
+ B_shape = x00.shape[:-2]
528
+ x00_flat = x00.reshape(-1, self.nlat, mmax)
529
+ x01_flat = x01.reshape(-1, self.nlat, mmax)
530
+ x10_flat = x10.reshape(-1, self.nlat, mmax)
531
+ x11_flat = x11.reshape(-1, self.nlat, mmax)
532
+ B = x00_flat.shape[0]
533
+
534
+ if self._use_bf16 and _HAS_CUDA_EXT and x00_flat.is_cuda and not x.requires_grad:
535
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0).bfloat16().contiguous()
536
+ out_w0 = fused_legendre_forward_real(stacked_w0, self.w0_t)
537
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
538
+
539
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0).bfloat16().contiguous()
540
+ out_w1 = fused_legendre_forward_real(stacked_w1, self.w1_t)
541
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
542
+ else:
543
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
544
+ if self._use_bf16:
545
+ stacked_w0 = stacked_w0.bfloat16()
546
+ out_w0 = torch.einsum("bkm,mlk->blm", stacked_w0, self.w0)
547
+ if self._use_bf16:
548
+ out_w0 = out_w0.float()
549
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
550
+
551
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
552
+ if self._use_bf16:
553
+ stacked_w1 = stacked_w1.bfloat16()
554
+ out_w1 = torch.einsum("bkm,mlk->blm", stacked_w1, self.w1)
555
+ if self._use_bf16:
556
+ out_w1 = out_w1.float()
557
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
558
+
559
+ sph_re = r00 - s11
560
+ sph_im = r01 + s10
561
+ tor_re = -s01 - r10
562
+ tor_im = s00 - r11
563
+
564
+ out_shape = list(B_shape) + [2, self.lmax, mmax, 2]
565
+ xout = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
566
+ xout[..., 0, :, :, 0] = sph_re.reshape(B_shape + (self.lmax, mmax))
567
+ xout[..., 0, :, :, 1] = sph_im.reshape(B_shape + (self.lmax, mmax))
568
+ xout[..., 1, :, :, 0] = tor_re.reshape(B_shape + (self.lmax, mmax))
569
+ xout[..., 1, :, :, 1] = tor_im.reshape(B_shape + (self.lmax, mmax))
570
+ return torch.view_as_complex(xout)
571
+
572
+
573
+ class InverseRealVectorSHT(nn.Module):
574
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealVectorSHT``."""
575
+
576
+ def __init__(
577
+ self,
578
+ nlat: int,
579
+ nlon: int,
580
+ lmax: Optional[int] = None,
581
+ mmax: Optional[int] = None,
582
+ grid: str = "equiangular",
583
+ norm: str = "ortho",
584
+ csphase: bool = True,
585
+ ):
586
+ super().__init__()
587
+ from torch_harmonics import InverseRealVectorSHT
588
+ ref = InverseRealVectorSHT(
589
+ nlat,
590
+ nlon,
591
+ lmax=lmax,
592
+ mmax=mmax,
593
+ grid=grid,
594
+ norm=norm,
595
+ csphase=csphase,
596
+ )
597
+ self.nlat = ref.nlat
598
+ self.nlon = ref.nlon
599
+ self.lmax = ref.lmax
600
+ self.mmax = ref.mmax
601
+ self.grid = grid
602
+ self.norm = norm
603
+ self.csphase = csphase
604
+ self.register_buffer("d0", ref.dpct[0].float()) # [mmax, lmax, nlat]
605
+ self.register_buffer("d1", ref.dpct[1].float()) # [mmax, lmax, nlat]
606
+ self.register_buffer("d0_t", ref.dpct[0].float().permute(1, 2, 0).contiguous())
607
+ self.register_buffer("d1_t", ref.dpct[1].float().permute(1, 2, 0).contiguous())
608
+
609
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
610
+ assert x.shape[-2] == self.lmax and x.shape[-1] == self.mmax
611
+
612
+ with _nvtx_range("holysht.vector_inverse"):
613
+ x = x.contiguous()
614
+ if _can_use_cuda_vector(x, self.d0_t, self.d1_t):
615
+ B_shape = x.shape[:-3]
616
+ x_flat = x.reshape(-1, 2, self.lmax, self.mmax).contiguous()
617
+ x_out = _FusedVectorLegendreInverseFn.apply(x_flat, self.d0_t, self.d1_t)
618
+ x_out = x_out.reshape(B_shape + (2, self.nlat, self.mmax))
619
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
620
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
621
+
622
+ x = torch.view_as_real(x) # [..., 2, lmax, mmax, 2]
623
+ mmax = self.mmax
624
+
625
+ x00 = x[..., 0, :, :, 0]
626
+ x01 = x[..., 0, :, :, 1]
627
+ x10 = x[..., 1, :, :, 0]
628
+ x11 = x[..., 1, :, :, 1]
629
+
630
+ B_shape = x00.shape[:-2]
631
+ x00_flat = x00.reshape(-1, self.lmax, mmax)
632
+ x01_flat = x01.reshape(-1, self.lmax, mmax)
633
+ x10_flat = x10.reshape(-1, self.lmax, mmax)
634
+ x11_flat = x11.reshape(-1, self.lmax, mmax)
635
+ B = x00_flat.shape[0]
636
+
637
+ stacked_d0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
638
+ out_d0 = torch.einsum("blm,mlk->bkm", stacked_d0, self.d0)
639
+ r00, r01, r10, r11 = out_d0[:B], out_d0[B:2 * B], out_d0[2 * B:3 * B], out_d0[3 * B:]
640
+
641
+ stacked_d1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
642
+ out_d1 = torch.einsum("blm,mlk->bkm", stacked_d1, self.d1)
643
+ s11, s10, s01, s00 = out_d1[:B], out_d1[B:2 * B], out_d1[2 * B:3 * B], out_d1[3 * B:]
644
+
645
+ srl = r00 - s11
646
+ sim = r01 + s10
647
+ trl = -s01 - r10
648
+ tim = s00 - r11
649
+
650
+ out_k = self.nlat
651
+ srl = srl.reshape(B_shape + (out_k, mmax))
652
+ sim = sim.reshape(B_shape + (out_k, mmax))
653
+ trl = trl.reshape(B_shape + (out_k, mmax))
654
+ tim = tim.reshape(B_shape + (out_k, mmax))
655
+
656
+ s = torch.stack((srl, sim), -1)
657
+ t = torch.stack((trl, tim), -1)
658
+ xs = torch.stack((s, t), -4)
659
+ x_out = torch.view_as_complex(xs)
660
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
661
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
662
+
663
+
664
+ legendre_forward = fused_legendre_forward
665
+ legendre_inverse = fused_legendre_inverse
666
+ sht_forward = fused_sht_forward
667
+ sht_inverse = fused_sht_inverse
668
+
669
+ # Backwards-compatible aliases from the research prototype.
670
+ FusedRealSHT = RealSHT
671
+ FusedInverseRealSHT = InverseRealSHT
672
+ FusedRealVectorSHT = RealVectorSHT
673
+ FusedInverseRealVectorSHT = InverseRealVectorSHT
build/torch210-cxx11-cu130-x86_64-linux/_holysht_cuda_53048d0.abi3.so ADDED
Binary file (132 Bytes). View file
 
build/torch210-cxx11-cu130-x86_64-linux/_jit_ops.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local CUDA extension loader for HOLYSHT.
2
+
3
+ This keeps development off the heavyweight `kernel-builder` path by compiling a
4
+ small torch extension for the current machine only. The compiled artefacts are
5
+ cached under `build/torch_extensions`.
6
+
7
+ Author: Chris von Csefalvay
8
+ Licence: MIT
9
+ Repository: https://github.com/chrisvoncsefalvay/holysht
10
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from torch.utils.cpp_extension import load
20
+
21
+
22
+ _ROOT = Path(__file__).resolve().parents[2]
23
+ _BUILD_DIR = _ROOT / "build" / "torch_extensions"
24
+ _EXTENSION_NAME = "holysht_ops"
25
+
26
+
27
+ def _default_arch_list() -> str:
28
+ if not torch.cuda.is_available():
29
+ return ""
30
+
31
+ major, minor = torch.cuda.get_device_capability(0)
32
+ if (major, minor) >= (12, 1):
33
+ return "12.0+PTX"
34
+ return f"{major}.{minor}"
35
+
36
+
37
+ def _cuda_flags() -> list[str]:
38
+ flags = [
39
+ "-O3",
40
+ "-lineinfo",
41
+ "-Xptxas=-warn-spills",
42
+ "--expt-relaxed-constexpr",
43
+ ]
44
+ if os.environ.get("HOLYSHT_USE_FAST_MATH", "1") != "0":
45
+ flags.append("--use_fast_math")
46
+ return flags
47
+
48
+
49
+ def _load_ops():
50
+ os.environ.setdefault("MAX_JOBS", "1")
51
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _default_arch_list())
52
+ _BUILD_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ load(
55
+ name=_EXTENSION_NAME,
56
+ sources=[
57
+ str(_ROOT / "torch-ext" / "torch_binding.cpp"),
58
+ str(_ROOT / "cuda" / "fused_legendre.cu"),
59
+ str(_ROOT / "cuda" / "fused_sht.cu"),
60
+ ],
61
+ extra_include_paths=[str(_ROOT / "torch-ext")],
62
+ extra_cflags=["-O3", "-std=c++17"],
63
+ extra_cuda_cflags=_cuda_flags(),
64
+ build_directory=str(_BUILD_DIR),
65
+ verbose=os.environ.get("HOLYSHT_VERBOSE_BUILD", "0") == "1",
66
+ with_cuda=True,
67
+ is_python_module=False,
68
+ )
69
+ return getattr(torch.ops, _EXTENSION_NAME)
70
+
71
+
72
+ ops = _load_ops()
build/torch210-cxx11-cu130-x86_64-linux/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _holysht_cuda_53048d0
3
+ ops = torch.ops._holysht_cuda_53048d0
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_holysht_cuda_53048d0::{op_name}"
build/torch210-cxx11-cu130-x86_64-linux/holysht/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch210-cxx11-cu130-x86_64-linux/metadata.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "license": "mit",
4
+ "upstream": "https://github.com/NVIDIA/torch-harmonics",
5
+ "python-depends": [],
6
+ "backend": {
7
+ "type": "cuda",
8
+ "archs": [
9
+ "12.0"
10
+ ]
11
+ }
12
+ }
build/torch211-cxx11-cu126-x86_64-linux/__init__.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HOLYSHT: Highly Optimised Legendre/Ylm/SHT.
2
+
3
+ CUDA-accelerated spherical harmonic transforms designed as a practical,
4
+ production-oriented companion to torch-harmonics.
5
+
6
+ Author: Chris von Csefalvay
7
+ Licence: MIT
8
+ Repository: https://github.com/chrisvoncsefalvay/holysht
9
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
10
+ """
11
+
12
+ import contextlib
13
+ import os
14
+ from typing import Optional
15
+ import torch
16
+ import torch.nn as nn
17
+
18
+ __all__ = [
19
+ "RealSHT",
20
+ "InverseRealSHT",
21
+ "RealVectorSHT",
22
+ "InverseRealVectorSHT",
23
+ "legendre_forward",
24
+ "legendre_inverse",
25
+ "sht_forward",
26
+ "sht_inverse",
27
+ ]
28
+
29
+ # Prefer kernel-builder's generated alias module on packaged builds, then fall
30
+ # back to the local single-machine JIT loader for development.
31
+ try:
32
+ from ._ops import ops as _ops
33
+ _HAS_CUDA_EXT = True
34
+ except ModuleNotFoundError:
35
+ try:
36
+ from ._jit_ops import ops as _ops
37
+ _HAS_CUDA_EXT = True
38
+ except ImportError:
39
+ _HAS_CUDA_EXT = False
40
+ except ImportError:
41
+ _HAS_CUDA_EXT = False
42
+
43
+
44
+ def _can_use_cuda_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
45
+ return (
46
+ _HAS_CUDA_EXT
47
+ and weight_t is not None
48
+ and input.is_cuda
49
+ and weight_t.is_cuda
50
+ and input.dtype == torch.complex64
51
+ and weight_t.dtype == torch.float32
52
+ and weight_t.is_contiguous()
53
+ )
54
+
55
+
56
+ def _can_use_cuda_real_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
57
+ return (
58
+ _HAS_CUDA_EXT
59
+ and weight_t is not None
60
+ and input.is_cuda
61
+ and weight_t.is_cuda
62
+ and input.dtype in (torch.float32, torch.bfloat16)
63
+ and weight_t.dtype == torch.float32
64
+ and input.is_contiguous()
65
+ and weight_t.is_contiguous()
66
+ )
67
+
68
+
69
+ def _can_use_cuda_vector(input: torch.Tensor, weight0_t: Optional[torch.Tensor], weight1_t: Optional[torch.Tensor]) -> bool:
70
+ return (
71
+ _HAS_CUDA_EXT
72
+ and weight0_t is not None
73
+ and weight1_t is not None
74
+ and input.is_cuda
75
+ and weight0_t.is_cuda
76
+ and weight1_t.is_cuda
77
+ and input.dtype == torch.complex64
78
+ and weight0_t.dtype == torch.float32
79
+ and weight1_t.dtype == torch.float32
80
+ and input.is_contiguous()
81
+ and weight0_t.is_contiguous()
82
+ and weight1_t.is_contiguous()
83
+ )
84
+
85
+
86
+ def _mul_i(x: torch.Tensor) -> torch.Tensor:
87
+ """Multiply a complex tensor by +i without promoting dtype."""
88
+ return torch.complex(-x.imag, x.real)
89
+
90
+
91
+ @contextlib.contextmanager
92
+ def _nvtx_range(name: str):
93
+ enabled = os.environ.get("HOLYSHT_ENABLE_NVTX", "0") == "1"
94
+ if enabled and torch.cuda.is_available():
95
+ torch.cuda.nvtx.range_push(name)
96
+ try:
97
+ yield
98
+ finally:
99
+ torch.cuda.nvtx.range_pop()
100
+ else:
101
+ yield
102
+
103
+
104
+ def _prepare_irfft_input(x: torch.Tensor, nlon: int, active_mmax: Optional[int] = None) -> torch.Tensor:
105
+ """Pad/clean an rFFT-format complex tensor before irfft."""
106
+ active_mmax = x.size(-1) if active_mmax is None else active_mmax
107
+ full_mmax = nlon // 2 + 1
108
+
109
+ if x.size(-1) == full_mmax:
110
+ out = x.contiguous()
111
+ else:
112
+ out_shape = list(x.shape)
113
+ out_shape[-1] = full_mmax
114
+ out = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
115
+ out[..., :x.size(-1)] = x
116
+
117
+ if _HAS_CUDA_EXT and out.is_cuda and out.dtype == torch.complex64:
118
+ orig_shape = out.shape
119
+ flat = out.reshape(-1, orig_shape[-2], orig_shape[-1]).contiguous()
120
+ _ops.sht_prepare_irfft(flat, active_mmax, nlon)
121
+ return flat.reshape(orig_shape)
122
+
123
+ out[..., 0] = out[..., 0].real.to(torch.complex64)
124
+ if nlon % 2 == 0:
125
+ nyquist_idx = nlon // 2
126
+ if nyquist_idx < out.size(-1):
127
+ out[..., nyquist_idx] = out[..., nyquist_idx].real.to(torch.complex64)
128
+ return out
129
+
130
+
131
+ class _FusedLegendreForwardFn(torch.autograd.Function):
132
+ @staticmethod
133
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
134
+ input_c = input.contiguous()
135
+ output = torch.empty(
136
+ input_c.size(0), weight_t.size(0), input_c.size(2),
137
+ device=input_c.device, dtype=torch.complex64
138
+ )
139
+ _ops.fused_legendre_forward(output, input_c, weight_t)
140
+ ctx.save_for_backward(weight_t)
141
+ return output
142
+
143
+ @staticmethod
144
+ def backward(ctx, grad_output: torch.Tensor):
145
+ (weight_t,) = ctx.saved_tensors
146
+ grad_output = grad_output.contiguous()
147
+ grad_input = torch.empty(
148
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
149
+ device=grad_output.device, dtype=torch.complex64
150
+ )
151
+ _ops.fused_legendre_inverse(grad_input, grad_output, weight_t)
152
+ return grad_input, None
153
+
154
+
155
+ class _FusedLegendreInverseFn(torch.autograd.Function):
156
+ @staticmethod
157
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
158
+ input_c = input.contiguous()
159
+ output = torch.empty(
160
+ input_c.size(0), weight_t.size(1), input_c.size(2),
161
+ device=input_c.device, dtype=torch.complex64
162
+ )
163
+ _ops.fused_legendre_inverse(output, input_c, weight_t)
164
+ ctx.save_for_backward(weight_t)
165
+ return output
166
+
167
+ @staticmethod
168
+ def backward(ctx, grad_output: torch.Tensor):
169
+ (weight_t,) = ctx.saved_tensors
170
+ grad_output = grad_output.contiguous()
171
+ grad_input = torch.empty(
172
+ grad_output.size(0), weight_t.size(0), grad_output.size(2),
173
+ device=grad_output.device, dtype=torch.complex64
174
+ )
175
+ _ops.fused_legendre_forward(grad_input, grad_output, weight_t)
176
+ return grad_input, None
177
+
178
+
179
+ class _FusedLegendreForwardRealFn(torch.autograd.Function):
180
+ @staticmethod
181
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
182
+ input_c = input.contiguous()
183
+ output = torch.empty(
184
+ input_c.size(0), weight_t.size(0), input_c.size(2),
185
+ device=input_c.device, dtype=torch.float32
186
+ )
187
+ _ops.fused_legendre_forward_real(output, input_c, weight_t)
188
+ ctx.save_for_backward(weight_t)
189
+ return output
190
+
191
+ @staticmethod
192
+ def backward(ctx, grad_output: torch.Tensor):
193
+ (weight_t,) = ctx.saved_tensors
194
+ grad_output = grad_output.contiguous()
195
+ grad_input = torch.empty(
196
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
197
+ device=grad_output.device, dtype=torch.float32
198
+ )
199
+ _ops.fused_legendre_inverse_real(grad_input, grad_output, weight_t)
200
+ return grad_input, None
201
+
202
+
203
+ class _FusedVectorLegendreForwardFn(torch.autograd.Function):
204
+ @staticmethod
205
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
206
+ input_c = input.contiguous()
207
+ output = torch.empty(
208
+ input_c.size(0), 2, weight0_t.size(0), input_c.size(3),
209
+ device=input_c.device, dtype=torch.complex64
210
+ )
211
+ _ops.fused_vector_legendre_forward(output, input_c, weight0_t, weight1_t)
212
+ ctx.save_for_backward(weight0_t, weight1_t)
213
+ return output
214
+
215
+ @staticmethod
216
+ def backward(ctx, grad_output: torch.Tensor):
217
+ weight0_t, weight1_t = ctx.saved_tensors
218
+ grad_output = grad_output.contiguous()
219
+ grad_input = torch.empty(
220
+ grad_output.size(0), 2, weight0_t.size(1), grad_output.size(3),
221
+ device=grad_output.device, dtype=torch.complex64
222
+ )
223
+ _ops.fused_vector_legendre_inverse(grad_input, grad_output, weight0_t, weight1_t)
224
+ return grad_input, None, None
225
+
226
+
227
+ class _FusedVectorLegendreInverseFn(torch.autograd.Function):
228
+ @staticmethod
229
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
230
+ input_c = input.contiguous()
231
+ output = torch.empty(
232
+ input_c.size(0), 2, weight0_t.size(1), input_c.size(3),
233
+ device=input_c.device, dtype=torch.complex64
234
+ )
235
+ _ops.fused_vector_legendre_inverse(output, input_c, weight0_t, weight1_t)
236
+ ctx.save_for_backward(weight0_t, weight1_t)
237
+ return output
238
+
239
+ @staticmethod
240
+ def backward(ctx, grad_output: torch.Tensor):
241
+ weight0_t, weight1_t = ctx.saved_tensors
242
+ grad_output = grad_output.contiguous()
243
+ grad_input = torch.empty(
244
+ grad_output.size(0), 2, weight0_t.size(0), grad_output.size(3),
245
+ device=grad_output.device, dtype=torch.complex64
246
+ )
247
+ _ops.fused_vector_legendre_forward(grad_input, grad_output, weight0_t, weight1_t)
248
+ return grad_input, None, None
249
+
250
+
251
+ # ============================================================================
252
+ # Fused Legendre Transform
253
+ # ============================================================================
254
+
255
+ def fused_legendre_forward(
256
+ input: torch.Tensor, # [B, nlat, mmax] complex64
257
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32 (original torch-harmonics layout)
258
+ weight_t: Optional[torch.Tensor] = None, # [lmax, nlat, mmax] pre-transposed
259
+ ) -> torch.Tensor:
260
+ """Fused forward Legendre transform operating on complex tensors.
261
+
262
+ Computes out[b,l,m] = Σ_k weights[m,l,k] · input[b,k,m] for complex input,
263
+ fusing the real and imaginary multiplications into a single pass.
264
+
265
+ Uses the custom CUDA kernels when the extension is available, otherwise
266
+ falls back to a stacked einsum.
267
+ """
268
+ B = input.size(0)
269
+ nlat = input.size(1)
270
+ mmax = input.size(2)
271
+ lmax = weights.size(1)
272
+
273
+ if weight_t is None:
274
+ weight_t = weights.float().permute(1, 2, 0).contiguous()
275
+
276
+ if _can_use_cuda_legendre(input, weight_t):
277
+ # Adaptive CUDA kernel: small-grid direct path + large-grid tiled path.
278
+ return _FusedLegendreForwardFn.apply(input, weight_t)
279
+ else:
280
+ # Fallback: stacked einsum (1.9x speedup over reference 2x einsum)
281
+ x = torch.view_as_real(input) # [B, nlat, mmax, 2]
282
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0) # [2B, nlat, mmax]
283
+ w = weights.to(x_stacked.dtype)
284
+ out_stacked = torch.einsum("bkm,mlk->blm", x_stacked, w)
285
+ out_re = out_stacked[:B]
286
+ out_im = out_stacked[B:]
287
+ return torch.complex(out_re, out_im)
288
+
289
+
290
+ def fused_legendre_inverse(
291
+ input: torch.Tensor, # [B, lmax, mmax] complex64
292
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
293
+ pct_t: Optional[torch.Tensor] = None,
294
+ ) -> torch.Tensor:
295
+ """Fused inverse Legendre transform."""
296
+ B = input.size(0)
297
+ lmax = input.size(1)
298
+ mmax = input.size(2)
299
+ nlat = pct.size(2)
300
+
301
+ if pct_t is None:
302
+ pct_t = pct.float().permute(1, 2, 0).contiguous()
303
+
304
+ if _can_use_cuda_legendre(input, pct_t):
305
+ return _FusedLegendreInverseFn.apply(input, pct_t)
306
+ else:
307
+ x = torch.view_as_real(input)
308
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0)
309
+ p = pct.to(x_stacked.dtype)
310
+ out_stacked = torch.einsum("blm,mlk->bkm", x_stacked, p)
311
+ return torch.complex(out_stacked[:B], out_stacked[B:])
312
+
313
+
314
+ def fused_legendre_forward_real(
315
+ input: torch.Tensor, # [B, nlat, mmax] float32 or bfloat16
316
+ weight_t: torch.Tensor, # [lmax, nlat, mmax] float32
317
+ ) -> torch.Tensor:
318
+ """Real-valued forward Legendre transform with float accumulation."""
319
+ if _can_use_cuda_real_legendre(input, weight_t):
320
+ return _FusedLegendreForwardRealFn.apply(input, weight_t)
321
+
322
+ return torch.einsum("bkm,lkm->blm", input.float(), weight_t)
323
+
324
+
325
+ # ============================================================================
326
+ # Fused SHT (complete pipeline)
327
+ # ============================================================================
328
+
329
+ def fused_sht_forward(
330
+ x: torch.Tensor, # [B, nlat, nlon] float32
331
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32
332
+ mmax: int,
333
+ weight_t: Optional[torch.Tensor] = None,
334
+ ) -> torch.Tensor:
335
+ """Complete fused forward SHT: rfft → fused Legendre → complex coefficients.
336
+
337
+ Replaces RealSHT.forward() with fewer intermediate allocations.
338
+ """
339
+ with _nvtx_range("holysht.scalar_forward"):
340
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
341
+ x_fft = x_fft[..., :mmax]
342
+ return fused_legendre_forward(x_fft, weights, weight_t)
343
+
344
+
345
+ def fused_sht_inverse(
346
+ coeffs: torch.Tensor, # [B, lmax, mmax] complex64
347
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
348
+ nlon: int,
349
+ pct_t: Optional[torch.Tensor] = None,
350
+ ) -> torch.Tensor:
351
+ """Complete fused inverse SHT: fused Legendre → zero-pad → irfft."""
352
+ with _nvtx_range("holysht.scalar_inverse"):
353
+ x = fused_legendre_inverse(coeffs, pct, pct_t)
354
+ x = _prepare_irfft_input(x, nlon, coeffs.size(-1))
355
+ return torch.fft.irfft(x, n=nlon, dim=-1, norm="forward")
356
+
357
+
358
+ # ============================================================================
359
+ # nn.Module wrappers (drop-in replacements for torch-harmonics)
360
+ # ============================================================================
361
+
362
+ class RealSHT(nn.Module):
363
+ """Optimised drop-in replacement for ``torch_harmonics.RealSHT``.
364
+
365
+ Args:
366
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ nlat: int,
372
+ nlon: int,
373
+ lmax: Optional[int] = None,
374
+ mmax: Optional[int] = None,
375
+ grid: str = "equiangular",
376
+ norm: str = "ortho",
377
+ csphase: bool = True,
378
+ dtype: str = "fp32",
379
+ ):
380
+ super().__init__()
381
+ from torch_harmonics import RealSHT
382
+ ref = RealSHT(
383
+ nlat,
384
+ nlon,
385
+ lmax=lmax,
386
+ mmax=mmax,
387
+ grid=grid,
388
+ norm=norm,
389
+ csphase=csphase,
390
+ )
391
+ self.nlat = ref.nlat
392
+ self.nlon = ref.nlon
393
+ self.lmax = ref.lmax
394
+ self.mmax = ref.mmax
395
+ self.grid = grid
396
+ self.norm = norm
397
+ self.csphase = csphase
398
+ self._use_bf16 = (dtype == "bf16")
399
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
400
+ self.register_buffer("weights", ref.weights.to(w_dtype))
401
+ self.register_buffer("weight_t", ref.weights.float().permute(1, 2, 0).contiguous())
402
+
403
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
404
+ if self._use_bf16:
405
+ with _nvtx_range("holysht.scalar_forward_bf16"):
406
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
407
+ x_fft = x_fft[..., :self.mmax]
408
+ xr = torch.view_as_real(x_fft)
409
+ if _HAS_CUDA_EXT and x.is_cuda and not x.requires_grad:
410
+ xr_bf16 = xr.bfloat16().contiguous()
411
+ out_re = fused_legendre_forward_real(xr_bf16[..., 0].contiguous(), self.weight_t)
412
+ out_im = fused_legendre_forward_real(xr_bf16[..., 1].contiguous(), self.weight_t)
413
+ return torch.complex(out_re, out_im)
414
+
415
+ B = x.size(0)
416
+ xs = torch.cat([xr[..., 0], xr[..., 1]], dim=0).bfloat16()
417
+ out = torch.einsum("bkm,mlk->blm", xs, self.weights).float()
418
+ return torch.complex(out[:B], out[B:])
419
+ return fused_sht_forward(x, self.weights, self.mmax, self.weight_t)
420
+
421
+
422
+ class InverseRealSHT(nn.Module):
423
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealSHT``."""
424
+
425
+ def __init__(
426
+ self,
427
+ nlat: int,
428
+ nlon: int,
429
+ lmax: Optional[int] = None,
430
+ mmax: Optional[int] = None,
431
+ grid: str = "equiangular",
432
+ norm: str = "ortho",
433
+ csphase: bool = True,
434
+ ):
435
+ super().__init__()
436
+ from torch_harmonics import InverseRealSHT
437
+ ref = InverseRealSHT(
438
+ nlat,
439
+ nlon,
440
+ lmax=lmax,
441
+ mmax=mmax,
442
+ grid=grid,
443
+ norm=norm,
444
+ csphase=csphase,
445
+ )
446
+ self.nlat = ref.nlat
447
+ self.nlon = ref.nlon
448
+ self.lmax = ref.lmax
449
+ self.mmax = ref.mmax
450
+ self.grid = grid
451
+ self.norm = norm
452
+ self.csphase = csphase
453
+ self.register_buffer("pct", ref.pct.float())
454
+ self.register_buffer("pct_t", ref.pct.float().permute(1, 2, 0).contiguous())
455
+
456
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
457
+ return fused_sht_inverse(x, self.pct, self.nlon, self.pct_t)
458
+
459
+
460
+ class RealVectorSHT(nn.Module):
461
+ """Optimised drop-in replacement for ``torch_harmonics.RealVectorSHT``.
462
+
463
+ Reduces eight reference einsums to two composed Legendre passes on the
464
+ default FP32 CUDA path.
465
+
466
+ Args:
467
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
468
+ """
469
+
470
+ def __init__(
471
+ self,
472
+ nlat: int,
473
+ nlon: int,
474
+ lmax: Optional[int] = None,
475
+ mmax: Optional[int] = None,
476
+ grid: str = "equiangular",
477
+ norm: str = "ortho",
478
+ csphase: bool = True,
479
+ dtype: str = "fp32",
480
+ ):
481
+ super().__init__()
482
+ from torch_harmonics import RealVectorSHT
483
+ ref = RealVectorSHT(
484
+ nlat,
485
+ nlon,
486
+ lmax=lmax,
487
+ mmax=mmax,
488
+ grid=grid,
489
+ norm=norm,
490
+ csphase=csphase,
491
+ )
492
+ self.nlat = ref.nlat
493
+ self.nlon = ref.nlon
494
+ self.lmax = ref.lmax
495
+ self.mmax = ref.mmax
496
+ self.grid = grid
497
+ self.norm = norm
498
+ self.csphase = csphase
499
+ self._use_bf16 = (dtype == "bf16")
500
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
501
+ self.register_buffer("w0", ref.weights[0].to(w_dtype)) # [mmax, lmax, nlat]
502
+ self.register_buffer("w1", ref.weights[1].to(w_dtype)) # [mmax, lmax, nlat]
503
+ self.register_buffer("w0_t", ref.weights[0].float().permute(1, 2, 0).contiguous())
504
+ self.register_buffer("w1_t", ref.weights[1].float().permute(1, 2, 0).contiguous())
505
+
506
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
507
+ assert x.shape[-2] == self.nlat and x.shape[-1] == self.nlon
508
+
509
+ with _nvtx_range("holysht.vector_forward"):
510
+ x = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
511
+ mmax = self.mmax
512
+ x = x[..., :mmax].contiguous()
513
+
514
+ if (not self._use_bf16) and _can_use_cuda_vector(x, self.w0_t, self.w1_t):
515
+ B_shape = x.shape[:-3]
516
+ x_flat = x.reshape(-1, 2, self.nlat, mmax).contiguous()
517
+ out = _FusedVectorLegendreForwardFn.apply(x_flat, self.w0_t, self.w1_t)
518
+ return out.reshape(B_shape + (2, self.lmax, mmax))
519
+
520
+ x = torch.view_as_real(x) # [..., 2, nlat, mmax, 2]
521
+
522
+ x00 = x[..., 0, :, :, 0]
523
+ x01 = x[..., 0, :, :, 1]
524
+ x10 = x[..., 1, :, :, 0]
525
+ x11 = x[..., 1, :, :, 1]
526
+
527
+ B_shape = x00.shape[:-2]
528
+ x00_flat = x00.reshape(-1, self.nlat, mmax)
529
+ x01_flat = x01.reshape(-1, self.nlat, mmax)
530
+ x10_flat = x10.reshape(-1, self.nlat, mmax)
531
+ x11_flat = x11.reshape(-1, self.nlat, mmax)
532
+ B = x00_flat.shape[0]
533
+
534
+ if self._use_bf16 and _HAS_CUDA_EXT and x00_flat.is_cuda and not x.requires_grad:
535
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0).bfloat16().contiguous()
536
+ out_w0 = fused_legendre_forward_real(stacked_w0, self.w0_t)
537
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
538
+
539
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0).bfloat16().contiguous()
540
+ out_w1 = fused_legendre_forward_real(stacked_w1, self.w1_t)
541
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
542
+ else:
543
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
544
+ if self._use_bf16:
545
+ stacked_w0 = stacked_w0.bfloat16()
546
+ out_w0 = torch.einsum("bkm,mlk->blm", stacked_w0, self.w0)
547
+ if self._use_bf16:
548
+ out_w0 = out_w0.float()
549
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
550
+
551
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
552
+ if self._use_bf16:
553
+ stacked_w1 = stacked_w1.bfloat16()
554
+ out_w1 = torch.einsum("bkm,mlk->blm", stacked_w1, self.w1)
555
+ if self._use_bf16:
556
+ out_w1 = out_w1.float()
557
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
558
+
559
+ sph_re = r00 - s11
560
+ sph_im = r01 + s10
561
+ tor_re = -s01 - r10
562
+ tor_im = s00 - r11
563
+
564
+ out_shape = list(B_shape) + [2, self.lmax, mmax, 2]
565
+ xout = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
566
+ xout[..., 0, :, :, 0] = sph_re.reshape(B_shape + (self.lmax, mmax))
567
+ xout[..., 0, :, :, 1] = sph_im.reshape(B_shape + (self.lmax, mmax))
568
+ xout[..., 1, :, :, 0] = tor_re.reshape(B_shape + (self.lmax, mmax))
569
+ xout[..., 1, :, :, 1] = tor_im.reshape(B_shape + (self.lmax, mmax))
570
+ return torch.view_as_complex(xout)
571
+
572
+
573
+ class InverseRealVectorSHT(nn.Module):
574
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealVectorSHT``."""
575
+
576
+ def __init__(
577
+ self,
578
+ nlat: int,
579
+ nlon: int,
580
+ lmax: Optional[int] = None,
581
+ mmax: Optional[int] = None,
582
+ grid: str = "equiangular",
583
+ norm: str = "ortho",
584
+ csphase: bool = True,
585
+ ):
586
+ super().__init__()
587
+ from torch_harmonics import InverseRealVectorSHT
588
+ ref = InverseRealVectorSHT(
589
+ nlat,
590
+ nlon,
591
+ lmax=lmax,
592
+ mmax=mmax,
593
+ grid=grid,
594
+ norm=norm,
595
+ csphase=csphase,
596
+ )
597
+ self.nlat = ref.nlat
598
+ self.nlon = ref.nlon
599
+ self.lmax = ref.lmax
600
+ self.mmax = ref.mmax
601
+ self.grid = grid
602
+ self.norm = norm
603
+ self.csphase = csphase
604
+ self.register_buffer("d0", ref.dpct[0].float()) # [mmax, lmax, nlat]
605
+ self.register_buffer("d1", ref.dpct[1].float()) # [mmax, lmax, nlat]
606
+ self.register_buffer("d0_t", ref.dpct[0].float().permute(1, 2, 0).contiguous())
607
+ self.register_buffer("d1_t", ref.dpct[1].float().permute(1, 2, 0).contiguous())
608
+
609
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
610
+ assert x.shape[-2] == self.lmax and x.shape[-1] == self.mmax
611
+
612
+ with _nvtx_range("holysht.vector_inverse"):
613
+ x = x.contiguous()
614
+ if _can_use_cuda_vector(x, self.d0_t, self.d1_t):
615
+ B_shape = x.shape[:-3]
616
+ x_flat = x.reshape(-1, 2, self.lmax, self.mmax).contiguous()
617
+ x_out = _FusedVectorLegendreInverseFn.apply(x_flat, self.d0_t, self.d1_t)
618
+ x_out = x_out.reshape(B_shape + (2, self.nlat, self.mmax))
619
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
620
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
621
+
622
+ x = torch.view_as_real(x) # [..., 2, lmax, mmax, 2]
623
+ mmax = self.mmax
624
+
625
+ x00 = x[..., 0, :, :, 0]
626
+ x01 = x[..., 0, :, :, 1]
627
+ x10 = x[..., 1, :, :, 0]
628
+ x11 = x[..., 1, :, :, 1]
629
+
630
+ B_shape = x00.shape[:-2]
631
+ x00_flat = x00.reshape(-1, self.lmax, mmax)
632
+ x01_flat = x01.reshape(-1, self.lmax, mmax)
633
+ x10_flat = x10.reshape(-1, self.lmax, mmax)
634
+ x11_flat = x11.reshape(-1, self.lmax, mmax)
635
+ B = x00_flat.shape[0]
636
+
637
+ stacked_d0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
638
+ out_d0 = torch.einsum("blm,mlk->bkm", stacked_d0, self.d0)
639
+ r00, r01, r10, r11 = out_d0[:B], out_d0[B:2 * B], out_d0[2 * B:3 * B], out_d0[3 * B:]
640
+
641
+ stacked_d1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
642
+ out_d1 = torch.einsum("blm,mlk->bkm", stacked_d1, self.d1)
643
+ s11, s10, s01, s00 = out_d1[:B], out_d1[B:2 * B], out_d1[2 * B:3 * B], out_d1[3 * B:]
644
+
645
+ srl = r00 - s11
646
+ sim = r01 + s10
647
+ trl = -s01 - r10
648
+ tim = s00 - r11
649
+
650
+ out_k = self.nlat
651
+ srl = srl.reshape(B_shape + (out_k, mmax))
652
+ sim = sim.reshape(B_shape + (out_k, mmax))
653
+ trl = trl.reshape(B_shape + (out_k, mmax))
654
+ tim = tim.reshape(B_shape + (out_k, mmax))
655
+
656
+ s = torch.stack((srl, sim), -1)
657
+ t = torch.stack((trl, tim), -1)
658
+ xs = torch.stack((s, t), -4)
659
+ x_out = torch.view_as_complex(xs)
660
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
661
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
662
+
663
+
664
+ legendre_forward = fused_legendre_forward
665
+ legendre_inverse = fused_legendre_inverse
666
+ sht_forward = fused_sht_forward
667
+ sht_inverse = fused_sht_inverse
668
+
669
+ # Backwards-compatible aliases from the research prototype.
670
+ FusedRealSHT = RealSHT
671
+ FusedInverseRealSHT = InverseRealSHT
672
+ FusedRealVectorSHT = RealVectorSHT
673
+ FusedInverseRealVectorSHT = InverseRealVectorSHT
build/torch211-cxx11-cu126-x86_64-linux/_holysht_cuda_53048d0.abi3.so ADDED
Binary file (132 Bytes). View file
 
build/torch211-cxx11-cu126-x86_64-linux/_jit_ops.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local CUDA extension loader for HOLYSHT.
2
+
3
+ This keeps development off the heavyweight `kernel-builder` path by compiling a
4
+ small torch extension for the current machine only. The compiled artefacts are
5
+ cached under `build/torch_extensions`.
6
+
7
+ Author: Chris von Csefalvay
8
+ Licence: MIT
9
+ Repository: https://github.com/chrisvoncsefalvay/holysht
10
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from torch.utils.cpp_extension import load
20
+
21
+
22
+ _ROOT = Path(__file__).resolve().parents[2]
23
+ _BUILD_DIR = _ROOT / "build" / "torch_extensions"
24
+ _EXTENSION_NAME = "holysht_ops"
25
+
26
+
27
+ def _default_arch_list() -> str:
28
+ if not torch.cuda.is_available():
29
+ return ""
30
+
31
+ major, minor = torch.cuda.get_device_capability(0)
32
+ if (major, minor) >= (12, 1):
33
+ return "12.0+PTX"
34
+ return f"{major}.{minor}"
35
+
36
+
37
+ def _cuda_flags() -> list[str]:
38
+ flags = [
39
+ "-O3",
40
+ "-lineinfo",
41
+ "-Xptxas=-warn-spills",
42
+ "--expt-relaxed-constexpr",
43
+ ]
44
+ if os.environ.get("HOLYSHT_USE_FAST_MATH", "1") != "0":
45
+ flags.append("--use_fast_math")
46
+ return flags
47
+
48
+
49
+ def _load_ops():
50
+ os.environ.setdefault("MAX_JOBS", "1")
51
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _default_arch_list())
52
+ _BUILD_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ load(
55
+ name=_EXTENSION_NAME,
56
+ sources=[
57
+ str(_ROOT / "torch-ext" / "torch_binding.cpp"),
58
+ str(_ROOT / "cuda" / "fused_legendre.cu"),
59
+ str(_ROOT / "cuda" / "fused_sht.cu"),
60
+ ],
61
+ extra_include_paths=[str(_ROOT / "torch-ext")],
62
+ extra_cflags=["-O3", "-std=c++17"],
63
+ extra_cuda_cflags=_cuda_flags(),
64
+ build_directory=str(_BUILD_DIR),
65
+ verbose=os.environ.get("HOLYSHT_VERBOSE_BUILD", "0") == "1",
66
+ with_cuda=True,
67
+ is_python_module=False,
68
+ )
69
+ return getattr(torch.ops, _EXTENSION_NAME)
70
+
71
+
72
+ ops = _load_ops()
build/torch211-cxx11-cu126-x86_64-linux/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _holysht_cuda_53048d0
3
+ ops = torch.ops._holysht_cuda_53048d0
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_holysht_cuda_53048d0::{op_name}"
build/torch211-cxx11-cu126-x86_64-linux/holysht/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch211-cxx11-cu126-x86_64-linux/metadata.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "license": "mit",
4
+ "upstream": "https://github.com/NVIDIA/torch-harmonics",
5
+ "python-depends": [],
6
+ "backend": {
7
+ "type": "cuda"
8
+ }
9
+ }
build/torch211-cxx11-cu128-x86_64-linux/__init__.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HOLYSHT: Highly Optimised Legendre/Ylm/SHT.
2
+
3
+ CUDA-accelerated spherical harmonic transforms designed as a practical,
4
+ production-oriented companion to torch-harmonics.
5
+
6
+ Author: Chris von Csefalvay
7
+ Licence: MIT
8
+ Repository: https://github.com/chrisvoncsefalvay/holysht
9
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
10
+ """
11
+
12
+ import contextlib
13
+ import os
14
+ from typing import Optional
15
+ import torch
16
+ import torch.nn as nn
17
+
18
+ __all__ = [
19
+ "RealSHT",
20
+ "InverseRealSHT",
21
+ "RealVectorSHT",
22
+ "InverseRealVectorSHT",
23
+ "legendre_forward",
24
+ "legendre_inverse",
25
+ "sht_forward",
26
+ "sht_inverse",
27
+ ]
28
+
29
+ # Prefer kernel-builder's generated alias module on packaged builds, then fall
30
+ # back to the local single-machine JIT loader for development.
31
+ try:
32
+ from ._ops import ops as _ops
33
+ _HAS_CUDA_EXT = True
34
+ except ModuleNotFoundError:
35
+ try:
36
+ from ._jit_ops import ops as _ops
37
+ _HAS_CUDA_EXT = True
38
+ except ImportError:
39
+ _HAS_CUDA_EXT = False
40
+ except ImportError:
41
+ _HAS_CUDA_EXT = False
42
+
43
+
44
+ def _can_use_cuda_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
45
+ return (
46
+ _HAS_CUDA_EXT
47
+ and weight_t is not None
48
+ and input.is_cuda
49
+ and weight_t.is_cuda
50
+ and input.dtype == torch.complex64
51
+ and weight_t.dtype == torch.float32
52
+ and weight_t.is_contiguous()
53
+ )
54
+
55
+
56
+ def _can_use_cuda_real_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
57
+ return (
58
+ _HAS_CUDA_EXT
59
+ and weight_t is not None
60
+ and input.is_cuda
61
+ and weight_t.is_cuda
62
+ and input.dtype in (torch.float32, torch.bfloat16)
63
+ and weight_t.dtype == torch.float32
64
+ and input.is_contiguous()
65
+ and weight_t.is_contiguous()
66
+ )
67
+
68
+
69
+ def _can_use_cuda_vector(input: torch.Tensor, weight0_t: Optional[torch.Tensor], weight1_t: Optional[torch.Tensor]) -> bool:
70
+ return (
71
+ _HAS_CUDA_EXT
72
+ and weight0_t is not None
73
+ and weight1_t is not None
74
+ and input.is_cuda
75
+ and weight0_t.is_cuda
76
+ and weight1_t.is_cuda
77
+ and input.dtype == torch.complex64
78
+ and weight0_t.dtype == torch.float32
79
+ and weight1_t.dtype == torch.float32
80
+ and input.is_contiguous()
81
+ and weight0_t.is_contiguous()
82
+ and weight1_t.is_contiguous()
83
+ )
84
+
85
+
86
+ def _mul_i(x: torch.Tensor) -> torch.Tensor:
87
+ """Multiply a complex tensor by +i without promoting dtype."""
88
+ return torch.complex(-x.imag, x.real)
89
+
90
+
91
+ @contextlib.contextmanager
92
+ def _nvtx_range(name: str):
93
+ enabled = os.environ.get("HOLYSHT_ENABLE_NVTX", "0") == "1"
94
+ if enabled and torch.cuda.is_available():
95
+ torch.cuda.nvtx.range_push(name)
96
+ try:
97
+ yield
98
+ finally:
99
+ torch.cuda.nvtx.range_pop()
100
+ else:
101
+ yield
102
+
103
+
104
+ def _prepare_irfft_input(x: torch.Tensor, nlon: int, active_mmax: Optional[int] = None) -> torch.Tensor:
105
+ """Pad/clean an rFFT-format complex tensor before irfft."""
106
+ active_mmax = x.size(-1) if active_mmax is None else active_mmax
107
+ full_mmax = nlon // 2 + 1
108
+
109
+ if x.size(-1) == full_mmax:
110
+ out = x.contiguous()
111
+ else:
112
+ out_shape = list(x.shape)
113
+ out_shape[-1] = full_mmax
114
+ out = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
115
+ out[..., :x.size(-1)] = x
116
+
117
+ if _HAS_CUDA_EXT and out.is_cuda and out.dtype == torch.complex64:
118
+ orig_shape = out.shape
119
+ flat = out.reshape(-1, orig_shape[-2], orig_shape[-1]).contiguous()
120
+ _ops.sht_prepare_irfft(flat, active_mmax, nlon)
121
+ return flat.reshape(orig_shape)
122
+
123
+ out[..., 0] = out[..., 0].real.to(torch.complex64)
124
+ if nlon % 2 == 0:
125
+ nyquist_idx = nlon // 2
126
+ if nyquist_idx < out.size(-1):
127
+ out[..., nyquist_idx] = out[..., nyquist_idx].real.to(torch.complex64)
128
+ return out
129
+
130
+
131
+ class _FusedLegendreForwardFn(torch.autograd.Function):
132
+ @staticmethod
133
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
134
+ input_c = input.contiguous()
135
+ output = torch.empty(
136
+ input_c.size(0), weight_t.size(0), input_c.size(2),
137
+ device=input_c.device, dtype=torch.complex64
138
+ )
139
+ _ops.fused_legendre_forward(output, input_c, weight_t)
140
+ ctx.save_for_backward(weight_t)
141
+ return output
142
+
143
+ @staticmethod
144
+ def backward(ctx, grad_output: torch.Tensor):
145
+ (weight_t,) = ctx.saved_tensors
146
+ grad_output = grad_output.contiguous()
147
+ grad_input = torch.empty(
148
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
149
+ device=grad_output.device, dtype=torch.complex64
150
+ )
151
+ _ops.fused_legendre_inverse(grad_input, grad_output, weight_t)
152
+ return grad_input, None
153
+
154
+
155
+ class _FusedLegendreInverseFn(torch.autograd.Function):
156
+ @staticmethod
157
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
158
+ input_c = input.contiguous()
159
+ output = torch.empty(
160
+ input_c.size(0), weight_t.size(1), input_c.size(2),
161
+ device=input_c.device, dtype=torch.complex64
162
+ )
163
+ _ops.fused_legendre_inverse(output, input_c, weight_t)
164
+ ctx.save_for_backward(weight_t)
165
+ return output
166
+
167
+ @staticmethod
168
+ def backward(ctx, grad_output: torch.Tensor):
169
+ (weight_t,) = ctx.saved_tensors
170
+ grad_output = grad_output.contiguous()
171
+ grad_input = torch.empty(
172
+ grad_output.size(0), weight_t.size(0), grad_output.size(2),
173
+ device=grad_output.device, dtype=torch.complex64
174
+ )
175
+ _ops.fused_legendre_forward(grad_input, grad_output, weight_t)
176
+ return grad_input, None
177
+
178
+
179
+ class _FusedLegendreForwardRealFn(torch.autograd.Function):
180
+ @staticmethod
181
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
182
+ input_c = input.contiguous()
183
+ output = torch.empty(
184
+ input_c.size(0), weight_t.size(0), input_c.size(2),
185
+ device=input_c.device, dtype=torch.float32
186
+ )
187
+ _ops.fused_legendre_forward_real(output, input_c, weight_t)
188
+ ctx.save_for_backward(weight_t)
189
+ return output
190
+
191
+ @staticmethod
192
+ def backward(ctx, grad_output: torch.Tensor):
193
+ (weight_t,) = ctx.saved_tensors
194
+ grad_output = grad_output.contiguous()
195
+ grad_input = torch.empty(
196
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
197
+ device=grad_output.device, dtype=torch.float32
198
+ )
199
+ _ops.fused_legendre_inverse_real(grad_input, grad_output, weight_t)
200
+ return grad_input, None
201
+
202
+
203
+ class _FusedVectorLegendreForwardFn(torch.autograd.Function):
204
+ @staticmethod
205
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
206
+ input_c = input.contiguous()
207
+ output = torch.empty(
208
+ input_c.size(0), 2, weight0_t.size(0), input_c.size(3),
209
+ device=input_c.device, dtype=torch.complex64
210
+ )
211
+ _ops.fused_vector_legendre_forward(output, input_c, weight0_t, weight1_t)
212
+ ctx.save_for_backward(weight0_t, weight1_t)
213
+ return output
214
+
215
+ @staticmethod
216
+ def backward(ctx, grad_output: torch.Tensor):
217
+ weight0_t, weight1_t = ctx.saved_tensors
218
+ grad_output = grad_output.contiguous()
219
+ grad_input = torch.empty(
220
+ grad_output.size(0), 2, weight0_t.size(1), grad_output.size(3),
221
+ device=grad_output.device, dtype=torch.complex64
222
+ )
223
+ _ops.fused_vector_legendre_inverse(grad_input, grad_output, weight0_t, weight1_t)
224
+ return grad_input, None, None
225
+
226
+
227
+ class _FusedVectorLegendreInverseFn(torch.autograd.Function):
228
+ @staticmethod
229
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
230
+ input_c = input.contiguous()
231
+ output = torch.empty(
232
+ input_c.size(0), 2, weight0_t.size(1), input_c.size(3),
233
+ device=input_c.device, dtype=torch.complex64
234
+ )
235
+ _ops.fused_vector_legendre_inverse(output, input_c, weight0_t, weight1_t)
236
+ ctx.save_for_backward(weight0_t, weight1_t)
237
+ return output
238
+
239
+ @staticmethod
240
+ def backward(ctx, grad_output: torch.Tensor):
241
+ weight0_t, weight1_t = ctx.saved_tensors
242
+ grad_output = grad_output.contiguous()
243
+ grad_input = torch.empty(
244
+ grad_output.size(0), 2, weight0_t.size(0), grad_output.size(3),
245
+ device=grad_output.device, dtype=torch.complex64
246
+ )
247
+ _ops.fused_vector_legendre_forward(grad_input, grad_output, weight0_t, weight1_t)
248
+ return grad_input, None, None
249
+
250
+
251
+ # ============================================================================
252
+ # Fused Legendre Transform
253
+ # ============================================================================
254
+
255
+ def fused_legendre_forward(
256
+ input: torch.Tensor, # [B, nlat, mmax] complex64
257
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32 (original torch-harmonics layout)
258
+ weight_t: Optional[torch.Tensor] = None, # [lmax, nlat, mmax] pre-transposed
259
+ ) -> torch.Tensor:
260
+ """Fused forward Legendre transform operating on complex tensors.
261
+
262
+ Computes out[b,l,m] = Σ_k weights[m,l,k] · input[b,k,m] for complex input,
263
+ fusing the real and imaginary multiplications into a single pass.
264
+
265
+ Uses the custom CUDA kernels when the extension is available, otherwise
266
+ falls back to a stacked einsum.
267
+ """
268
+ B = input.size(0)
269
+ nlat = input.size(1)
270
+ mmax = input.size(2)
271
+ lmax = weights.size(1)
272
+
273
+ if weight_t is None:
274
+ weight_t = weights.float().permute(1, 2, 0).contiguous()
275
+
276
+ if _can_use_cuda_legendre(input, weight_t):
277
+ # Adaptive CUDA kernel: small-grid direct path + large-grid tiled path.
278
+ return _FusedLegendreForwardFn.apply(input, weight_t)
279
+ else:
280
+ # Fallback: stacked einsum (1.9x speedup over reference 2x einsum)
281
+ x = torch.view_as_real(input) # [B, nlat, mmax, 2]
282
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0) # [2B, nlat, mmax]
283
+ w = weights.to(x_stacked.dtype)
284
+ out_stacked = torch.einsum("bkm,mlk->blm", x_stacked, w)
285
+ out_re = out_stacked[:B]
286
+ out_im = out_stacked[B:]
287
+ return torch.complex(out_re, out_im)
288
+
289
+
290
+ def fused_legendre_inverse(
291
+ input: torch.Tensor, # [B, lmax, mmax] complex64
292
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
293
+ pct_t: Optional[torch.Tensor] = None,
294
+ ) -> torch.Tensor:
295
+ """Fused inverse Legendre transform."""
296
+ B = input.size(0)
297
+ lmax = input.size(1)
298
+ mmax = input.size(2)
299
+ nlat = pct.size(2)
300
+
301
+ if pct_t is None:
302
+ pct_t = pct.float().permute(1, 2, 0).contiguous()
303
+
304
+ if _can_use_cuda_legendre(input, pct_t):
305
+ return _FusedLegendreInverseFn.apply(input, pct_t)
306
+ else:
307
+ x = torch.view_as_real(input)
308
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0)
309
+ p = pct.to(x_stacked.dtype)
310
+ out_stacked = torch.einsum("blm,mlk->bkm", x_stacked, p)
311
+ return torch.complex(out_stacked[:B], out_stacked[B:])
312
+
313
+
314
+ def fused_legendre_forward_real(
315
+ input: torch.Tensor, # [B, nlat, mmax] float32 or bfloat16
316
+ weight_t: torch.Tensor, # [lmax, nlat, mmax] float32
317
+ ) -> torch.Tensor:
318
+ """Real-valued forward Legendre transform with float accumulation."""
319
+ if _can_use_cuda_real_legendre(input, weight_t):
320
+ return _FusedLegendreForwardRealFn.apply(input, weight_t)
321
+
322
+ return torch.einsum("bkm,lkm->blm", input.float(), weight_t)
323
+
324
+
325
+ # ============================================================================
326
+ # Fused SHT (complete pipeline)
327
+ # ============================================================================
328
+
329
+ def fused_sht_forward(
330
+ x: torch.Tensor, # [B, nlat, nlon] float32
331
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32
332
+ mmax: int,
333
+ weight_t: Optional[torch.Tensor] = None,
334
+ ) -> torch.Tensor:
335
+ """Complete fused forward SHT: rfft → fused Legendre → complex coefficients.
336
+
337
+ Replaces RealSHT.forward() with fewer intermediate allocations.
338
+ """
339
+ with _nvtx_range("holysht.scalar_forward"):
340
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
341
+ x_fft = x_fft[..., :mmax]
342
+ return fused_legendre_forward(x_fft, weights, weight_t)
343
+
344
+
345
+ def fused_sht_inverse(
346
+ coeffs: torch.Tensor, # [B, lmax, mmax] complex64
347
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
348
+ nlon: int,
349
+ pct_t: Optional[torch.Tensor] = None,
350
+ ) -> torch.Tensor:
351
+ """Complete fused inverse SHT: fused Legendre → zero-pad → irfft."""
352
+ with _nvtx_range("holysht.scalar_inverse"):
353
+ x = fused_legendre_inverse(coeffs, pct, pct_t)
354
+ x = _prepare_irfft_input(x, nlon, coeffs.size(-1))
355
+ return torch.fft.irfft(x, n=nlon, dim=-1, norm="forward")
356
+
357
+
358
+ # ============================================================================
359
+ # nn.Module wrappers (drop-in replacements for torch-harmonics)
360
+ # ============================================================================
361
+
362
+ class RealSHT(nn.Module):
363
+ """Optimised drop-in replacement for ``torch_harmonics.RealSHT``.
364
+
365
+ Args:
366
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ nlat: int,
372
+ nlon: int,
373
+ lmax: Optional[int] = None,
374
+ mmax: Optional[int] = None,
375
+ grid: str = "equiangular",
376
+ norm: str = "ortho",
377
+ csphase: bool = True,
378
+ dtype: str = "fp32",
379
+ ):
380
+ super().__init__()
381
+ from torch_harmonics import RealSHT
382
+ ref = RealSHT(
383
+ nlat,
384
+ nlon,
385
+ lmax=lmax,
386
+ mmax=mmax,
387
+ grid=grid,
388
+ norm=norm,
389
+ csphase=csphase,
390
+ )
391
+ self.nlat = ref.nlat
392
+ self.nlon = ref.nlon
393
+ self.lmax = ref.lmax
394
+ self.mmax = ref.mmax
395
+ self.grid = grid
396
+ self.norm = norm
397
+ self.csphase = csphase
398
+ self._use_bf16 = (dtype == "bf16")
399
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
400
+ self.register_buffer("weights", ref.weights.to(w_dtype))
401
+ self.register_buffer("weight_t", ref.weights.float().permute(1, 2, 0).contiguous())
402
+
403
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
404
+ if self._use_bf16:
405
+ with _nvtx_range("holysht.scalar_forward_bf16"):
406
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
407
+ x_fft = x_fft[..., :self.mmax]
408
+ xr = torch.view_as_real(x_fft)
409
+ if _HAS_CUDA_EXT and x.is_cuda and not x.requires_grad:
410
+ xr_bf16 = xr.bfloat16().contiguous()
411
+ out_re = fused_legendre_forward_real(xr_bf16[..., 0].contiguous(), self.weight_t)
412
+ out_im = fused_legendre_forward_real(xr_bf16[..., 1].contiguous(), self.weight_t)
413
+ return torch.complex(out_re, out_im)
414
+
415
+ B = x.size(0)
416
+ xs = torch.cat([xr[..., 0], xr[..., 1]], dim=0).bfloat16()
417
+ out = torch.einsum("bkm,mlk->blm", xs, self.weights).float()
418
+ return torch.complex(out[:B], out[B:])
419
+ return fused_sht_forward(x, self.weights, self.mmax, self.weight_t)
420
+
421
+
422
+ class InverseRealSHT(nn.Module):
423
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealSHT``."""
424
+
425
+ def __init__(
426
+ self,
427
+ nlat: int,
428
+ nlon: int,
429
+ lmax: Optional[int] = None,
430
+ mmax: Optional[int] = None,
431
+ grid: str = "equiangular",
432
+ norm: str = "ortho",
433
+ csphase: bool = True,
434
+ ):
435
+ super().__init__()
436
+ from torch_harmonics import InverseRealSHT
437
+ ref = InverseRealSHT(
438
+ nlat,
439
+ nlon,
440
+ lmax=lmax,
441
+ mmax=mmax,
442
+ grid=grid,
443
+ norm=norm,
444
+ csphase=csphase,
445
+ )
446
+ self.nlat = ref.nlat
447
+ self.nlon = ref.nlon
448
+ self.lmax = ref.lmax
449
+ self.mmax = ref.mmax
450
+ self.grid = grid
451
+ self.norm = norm
452
+ self.csphase = csphase
453
+ self.register_buffer("pct", ref.pct.float())
454
+ self.register_buffer("pct_t", ref.pct.float().permute(1, 2, 0).contiguous())
455
+
456
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
457
+ return fused_sht_inverse(x, self.pct, self.nlon, self.pct_t)
458
+
459
+
460
+ class RealVectorSHT(nn.Module):
461
+ """Optimised drop-in replacement for ``torch_harmonics.RealVectorSHT``.
462
+
463
+ Reduces eight reference einsums to two composed Legendre passes on the
464
+ default FP32 CUDA path.
465
+
466
+ Args:
467
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
468
+ """
469
+
470
+ def __init__(
471
+ self,
472
+ nlat: int,
473
+ nlon: int,
474
+ lmax: Optional[int] = None,
475
+ mmax: Optional[int] = None,
476
+ grid: str = "equiangular",
477
+ norm: str = "ortho",
478
+ csphase: bool = True,
479
+ dtype: str = "fp32",
480
+ ):
481
+ super().__init__()
482
+ from torch_harmonics import RealVectorSHT
483
+ ref = RealVectorSHT(
484
+ nlat,
485
+ nlon,
486
+ lmax=lmax,
487
+ mmax=mmax,
488
+ grid=grid,
489
+ norm=norm,
490
+ csphase=csphase,
491
+ )
492
+ self.nlat = ref.nlat
493
+ self.nlon = ref.nlon
494
+ self.lmax = ref.lmax
495
+ self.mmax = ref.mmax
496
+ self.grid = grid
497
+ self.norm = norm
498
+ self.csphase = csphase
499
+ self._use_bf16 = (dtype == "bf16")
500
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
501
+ self.register_buffer("w0", ref.weights[0].to(w_dtype)) # [mmax, lmax, nlat]
502
+ self.register_buffer("w1", ref.weights[1].to(w_dtype)) # [mmax, lmax, nlat]
503
+ self.register_buffer("w0_t", ref.weights[0].float().permute(1, 2, 0).contiguous())
504
+ self.register_buffer("w1_t", ref.weights[1].float().permute(1, 2, 0).contiguous())
505
+
506
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
507
+ assert x.shape[-2] == self.nlat and x.shape[-1] == self.nlon
508
+
509
+ with _nvtx_range("holysht.vector_forward"):
510
+ x = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
511
+ mmax = self.mmax
512
+ x = x[..., :mmax].contiguous()
513
+
514
+ if (not self._use_bf16) and _can_use_cuda_vector(x, self.w0_t, self.w1_t):
515
+ B_shape = x.shape[:-3]
516
+ x_flat = x.reshape(-1, 2, self.nlat, mmax).contiguous()
517
+ out = _FusedVectorLegendreForwardFn.apply(x_flat, self.w0_t, self.w1_t)
518
+ return out.reshape(B_shape + (2, self.lmax, mmax))
519
+
520
+ x = torch.view_as_real(x) # [..., 2, nlat, mmax, 2]
521
+
522
+ x00 = x[..., 0, :, :, 0]
523
+ x01 = x[..., 0, :, :, 1]
524
+ x10 = x[..., 1, :, :, 0]
525
+ x11 = x[..., 1, :, :, 1]
526
+
527
+ B_shape = x00.shape[:-2]
528
+ x00_flat = x00.reshape(-1, self.nlat, mmax)
529
+ x01_flat = x01.reshape(-1, self.nlat, mmax)
530
+ x10_flat = x10.reshape(-1, self.nlat, mmax)
531
+ x11_flat = x11.reshape(-1, self.nlat, mmax)
532
+ B = x00_flat.shape[0]
533
+
534
+ if self._use_bf16 and _HAS_CUDA_EXT and x00_flat.is_cuda and not x.requires_grad:
535
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0).bfloat16().contiguous()
536
+ out_w0 = fused_legendre_forward_real(stacked_w0, self.w0_t)
537
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
538
+
539
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0).bfloat16().contiguous()
540
+ out_w1 = fused_legendre_forward_real(stacked_w1, self.w1_t)
541
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
542
+ else:
543
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
544
+ if self._use_bf16:
545
+ stacked_w0 = stacked_w0.bfloat16()
546
+ out_w0 = torch.einsum("bkm,mlk->blm", stacked_w0, self.w0)
547
+ if self._use_bf16:
548
+ out_w0 = out_w0.float()
549
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
550
+
551
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
552
+ if self._use_bf16:
553
+ stacked_w1 = stacked_w1.bfloat16()
554
+ out_w1 = torch.einsum("bkm,mlk->blm", stacked_w1, self.w1)
555
+ if self._use_bf16:
556
+ out_w1 = out_w1.float()
557
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
558
+
559
+ sph_re = r00 - s11
560
+ sph_im = r01 + s10
561
+ tor_re = -s01 - r10
562
+ tor_im = s00 - r11
563
+
564
+ out_shape = list(B_shape) + [2, self.lmax, mmax, 2]
565
+ xout = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
566
+ xout[..., 0, :, :, 0] = sph_re.reshape(B_shape + (self.lmax, mmax))
567
+ xout[..., 0, :, :, 1] = sph_im.reshape(B_shape + (self.lmax, mmax))
568
+ xout[..., 1, :, :, 0] = tor_re.reshape(B_shape + (self.lmax, mmax))
569
+ xout[..., 1, :, :, 1] = tor_im.reshape(B_shape + (self.lmax, mmax))
570
+ return torch.view_as_complex(xout)
571
+
572
+
573
+ class InverseRealVectorSHT(nn.Module):
574
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealVectorSHT``."""
575
+
576
+ def __init__(
577
+ self,
578
+ nlat: int,
579
+ nlon: int,
580
+ lmax: Optional[int] = None,
581
+ mmax: Optional[int] = None,
582
+ grid: str = "equiangular",
583
+ norm: str = "ortho",
584
+ csphase: bool = True,
585
+ ):
586
+ super().__init__()
587
+ from torch_harmonics import InverseRealVectorSHT
588
+ ref = InverseRealVectorSHT(
589
+ nlat,
590
+ nlon,
591
+ lmax=lmax,
592
+ mmax=mmax,
593
+ grid=grid,
594
+ norm=norm,
595
+ csphase=csphase,
596
+ )
597
+ self.nlat = ref.nlat
598
+ self.nlon = ref.nlon
599
+ self.lmax = ref.lmax
600
+ self.mmax = ref.mmax
601
+ self.grid = grid
602
+ self.norm = norm
603
+ self.csphase = csphase
604
+ self.register_buffer("d0", ref.dpct[0].float()) # [mmax, lmax, nlat]
605
+ self.register_buffer("d1", ref.dpct[1].float()) # [mmax, lmax, nlat]
606
+ self.register_buffer("d0_t", ref.dpct[0].float().permute(1, 2, 0).contiguous())
607
+ self.register_buffer("d1_t", ref.dpct[1].float().permute(1, 2, 0).contiguous())
608
+
609
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
610
+ assert x.shape[-2] == self.lmax and x.shape[-1] == self.mmax
611
+
612
+ with _nvtx_range("holysht.vector_inverse"):
613
+ x = x.contiguous()
614
+ if _can_use_cuda_vector(x, self.d0_t, self.d1_t):
615
+ B_shape = x.shape[:-3]
616
+ x_flat = x.reshape(-1, 2, self.lmax, self.mmax).contiguous()
617
+ x_out = _FusedVectorLegendreInverseFn.apply(x_flat, self.d0_t, self.d1_t)
618
+ x_out = x_out.reshape(B_shape + (2, self.nlat, self.mmax))
619
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
620
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
621
+
622
+ x = torch.view_as_real(x) # [..., 2, lmax, mmax, 2]
623
+ mmax = self.mmax
624
+
625
+ x00 = x[..., 0, :, :, 0]
626
+ x01 = x[..., 0, :, :, 1]
627
+ x10 = x[..., 1, :, :, 0]
628
+ x11 = x[..., 1, :, :, 1]
629
+
630
+ B_shape = x00.shape[:-2]
631
+ x00_flat = x00.reshape(-1, self.lmax, mmax)
632
+ x01_flat = x01.reshape(-1, self.lmax, mmax)
633
+ x10_flat = x10.reshape(-1, self.lmax, mmax)
634
+ x11_flat = x11.reshape(-1, self.lmax, mmax)
635
+ B = x00_flat.shape[0]
636
+
637
+ stacked_d0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
638
+ out_d0 = torch.einsum("blm,mlk->bkm", stacked_d0, self.d0)
639
+ r00, r01, r10, r11 = out_d0[:B], out_d0[B:2 * B], out_d0[2 * B:3 * B], out_d0[3 * B:]
640
+
641
+ stacked_d1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
642
+ out_d1 = torch.einsum("blm,mlk->bkm", stacked_d1, self.d1)
643
+ s11, s10, s01, s00 = out_d1[:B], out_d1[B:2 * B], out_d1[2 * B:3 * B], out_d1[3 * B:]
644
+
645
+ srl = r00 - s11
646
+ sim = r01 + s10
647
+ trl = -s01 - r10
648
+ tim = s00 - r11
649
+
650
+ out_k = self.nlat
651
+ srl = srl.reshape(B_shape + (out_k, mmax))
652
+ sim = sim.reshape(B_shape + (out_k, mmax))
653
+ trl = trl.reshape(B_shape + (out_k, mmax))
654
+ tim = tim.reshape(B_shape + (out_k, mmax))
655
+
656
+ s = torch.stack((srl, sim), -1)
657
+ t = torch.stack((trl, tim), -1)
658
+ xs = torch.stack((s, t), -4)
659
+ x_out = torch.view_as_complex(xs)
660
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
661
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
662
+
663
+
664
+ legendre_forward = fused_legendre_forward
665
+ legendre_inverse = fused_legendre_inverse
666
+ sht_forward = fused_sht_forward
667
+ sht_inverse = fused_sht_inverse
668
+
669
+ # Backwards-compatible aliases from the research prototype.
670
+ FusedRealSHT = RealSHT
671
+ FusedInverseRealSHT = InverseRealSHT
672
+ FusedRealVectorSHT = RealVectorSHT
673
+ FusedInverseRealVectorSHT = InverseRealVectorSHT
build/torch211-cxx11-cu128-x86_64-linux/_holysht_cuda_53048d0.abi3.so ADDED
Binary file (132 Bytes). View file
 
build/torch211-cxx11-cu128-x86_64-linux/_jit_ops.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local CUDA extension loader for HOLYSHT.
2
+
3
+ This keeps development off the heavyweight `kernel-builder` path by compiling a
4
+ small torch extension for the current machine only. The compiled artefacts are
5
+ cached under `build/torch_extensions`.
6
+
7
+ Author: Chris von Csefalvay
8
+ Licence: MIT
9
+ Repository: https://github.com/chrisvoncsefalvay/holysht
10
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from torch.utils.cpp_extension import load
20
+
21
+
22
+ _ROOT = Path(__file__).resolve().parents[2]
23
+ _BUILD_DIR = _ROOT / "build" / "torch_extensions"
24
+ _EXTENSION_NAME = "holysht_ops"
25
+
26
+
27
+ def _default_arch_list() -> str:
28
+ if not torch.cuda.is_available():
29
+ return ""
30
+
31
+ major, minor = torch.cuda.get_device_capability(0)
32
+ if (major, minor) >= (12, 1):
33
+ return "12.0+PTX"
34
+ return f"{major}.{minor}"
35
+
36
+
37
+ def _cuda_flags() -> list[str]:
38
+ flags = [
39
+ "-O3",
40
+ "-lineinfo",
41
+ "-Xptxas=-warn-spills",
42
+ "--expt-relaxed-constexpr",
43
+ ]
44
+ if os.environ.get("HOLYSHT_USE_FAST_MATH", "1") != "0":
45
+ flags.append("--use_fast_math")
46
+ return flags
47
+
48
+
49
+ def _load_ops():
50
+ os.environ.setdefault("MAX_JOBS", "1")
51
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _default_arch_list())
52
+ _BUILD_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ load(
55
+ name=_EXTENSION_NAME,
56
+ sources=[
57
+ str(_ROOT / "torch-ext" / "torch_binding.cpp"),
58
+ str(_ROOT / "cuda" / "fused_legendre.cu"),
59
+ str(_ROOT / "cuda" / "fused_sht.cu"),
60
+ ],
61
+ extra_include_paths=[str(_ROOT / "torch-ext")],
62
+ extra_cflags=["-O3", "-std=c++17"],
63
+ extra_cuda_cflags=_cuda_flags(),
64
+ build_directory=str(_BUILD_DIR),
65
+ verbose=os.environ.get("HOLYSHT_VERBOSE_BUILD", "0") == "1",
66
+ with_cuda=True,
67
+ is_python_module=False,
68
+ )
69
+ return getattr(torch.ops, _EXTENSION_NAME)
70
+
71
+
72
+ ops = _load_ops()
build/torch211-cxx11-cu128-x86_64-linux/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _holysht_cuda_53048d0
3
+ ops = torch.ops._holysht_cuda_53048d0
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_holysht_cuda_53048d0::{op_name}"
build/torch211-cxx11-cu128-x86_64-linux/holysht/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch211-cxx11-cu128-x86_64-linux/metadata.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "license": "mit",
4
+ "upstream": "https://github.com/NVIDIA/torch-harmonics",
5
+ "python-depends": [],
6
+ "backend": {
7
+ "type": "cuda",
8
+ "archs": [
9
+ "12.0"
10
+ ]
11
+ }
12
+ }
build/torch211-cxx11-cu130-x86_64-linux/__init__.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HOLYSHT: Highly Optimised Legendre/Ylm/SHT.
2
+
3
+ CUDA-accelerated spherical harmonic transforms designed as a practical,
4
+ production-oriented companion to torch-harmonics.
5
+
6
+ Author: Chris von Csefalvay
7
+ Licence: MIT
8
+ Repository: https://github.com/chrisvoncsefalvay/holysht
9
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
10
+ """
11
+
12
+ import contextlib
13
+ import os
14
+ from typing import Optional
15
+ import torch
16
+ import torch.nn as nn
17
+
18
+ __all__ = [
19
+ "RealSHT",
20
+ "InverseRealSHT",
21
+ "RealVectorSHT",
22
+ "InverseRealVectorSHT",
23
+ "legendre_forward",
24
+ "legendre_inverse",
25
+ "sht_forward",
26
+ "sht_inverse",
27
+ ]
28
+
29
+ # Prefer kernel-builder's generated alias module on packaged builds, then fall
30
+ # back to the local single-machine JIT loader for development.
31
+ try:
32
+ from ._ops import ops as _ops
33
+ _HAS_CUDA_EXT = True
34
+ except ModuleNotFoundError:
35
+ try:
36
+ from ._jit_ops import ops as _ops
37
+ _HAS_CUDA_EXT = True
38
+ except ImportError:
39
+ _HAS_CUDA_EXT = False
40
+ except ImportError:
41
+ _HAS_CUDA_EXT = False
42
+
43
+
44
+ def _can_use_cuda_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
45
+ return (
46
+ _HAS_CUDA_EXT
47
+ and weight_t is not None
48
+ and input.is_cuda
49
+ and weight_t.is_cuda
50
+ and input.dtype == torch.complex64
51
+ and weight_t.dtype == torch.float32
52
+ and weight_t.is_contiguous()
53
+ )
54
+
55
+
56
+ def _can_use_cuda_real_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
57
+ return (
58
+ _HAS_CUDA_EXT
59
+ and weight_t is not None
60
+ and input.is_cuda
61
+ and weight_t.is_cuda
62
+ and input.dtype in (torch.float32, torch.bfloat16)
63
+ and weight_t.dtype == torch.float32
64
+ and input.is_contiguous()
65
+ and weight_t.is_contiguous()
66
+ )
67
+
68
+
69
+ def _can_use_cuda_vector(input: torch.Tensor, weight0_t: Optional[torch.Tensor], weight1_t: Optional[torch.Tensor]) -> bool:
70
+ return (
71
+ _HAS_CUDA_EXT
72
+ and weight0_t is not None
73
+ and weight1_t is not None
74
+ and input.is_cuda
75
+ and weight0_t.is_cuda
76
+ and weight1_t.is_cuda
77
+ and input.dtype == torch.complex64
78
+ and weight0_t.dtype == torch.float32
79
+ and weight1_t.dtype == torch.float32
80
+ and input.is_contiguous()
81
+ and weight0_t.is_contiguous()
82
+ and weight1_t.is_contiguous()
83
+ )
84
+
85
+
86
+ def _mul_i(x: torch.Tensor) -> torch.Tensor:
87
+ """Multiply a complex tensor by +i without promoting dtype."""
88
+ return torch.complex(-x.imag, x.real)
89
+
90
+
91
+ @contextlib.contextmanager
92
+ def _nvtx_range(name: str):
93
+ enabled = os.environ.get("HOLYSHT_ENABLE_NVTX", "0") == "1"
94
+ if enabled and torch.cuda.is_available():
95
+ torch.cuda.nvtx.range_push(name)
96
+ try:
97
+ yield
98
+ finally:
99
+ torch.cuda.nvtx.range_pop()
100
+ else:
101
+ yield
102
+
103
+
104
+ def _prepare_irfft_input(x: torch.Tensor, nlon: int, active_mmax: Optional[int] = None) -> torch.Tensor:
105
+ """Pad/clean an rFFT-format complex tensor before irfft."""
106
+ active_mmax = x.size(-1) if active_mmax is None else active_mmax
107
+ full_mmax = nlon // 2 + 1
108
+
109
+ if x.size(-1) == full_mmax:
110
+ out = x.contiguous()
111
+ else:
112
+ out_shape = list(x.shape)
113
+ out_shape[-1] = full_mmax
114
+ out = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
115
+ out[..., :x.size(-1)] = x
116
+
117
+ if _HAS_CUDA_EXT and out.is_cuda and out.dtype == torch.complex64:
118
+ orig_shape = out.shape
119
+ flat = out.reshape(-1, orig_shape[-2], orig_shape[-1]).contiguous()
120
+ _ops.sht_prepare_irfft(flat, active_mmax, nlon)
121
+ return flat.reshape(orig_shape)
122
+
123
+ out[..., 0] = out[..., 0].real.to(torch.complex64)
124
+ if nlon % 2 == 0:
125
+ nyquist_idx = nlon // 2
126
+ if nyquist_idx < out.size(-1):
127
+ out[..., nyquist_idx] = out[..., nyquist_idx].real.to(torch.complex64)
128
+ return out
129
+
130
+
131
+ class _FusedLegendreForwardFn(torch.autograd.Function):
132
+ @staticmethod
133
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
134
+ input_c = input.contiguous()
135
+ output = torch.empty(
136
+ input_c.size(0), weight_t.size(0), input_c.size(2),
137
+ device=input_c.device, dtype=torch.complex64
138
+ )
139
+ _ops.fused_legendre_forward(output, input_c, weight_t)
140
+ ctx.save_for_backward(weight_t)
141
+ return output
142
+
143
+ @staticmethod
144
+ def backward(ctx, grad_output: torch.Tensor):
145
+ (weight_t,) = ctx.saved_tensors
146
+ grad_output = grad_output.contiguous()
147
+ grad_input = torch.empty(
148
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
149
+ device=grad_output.device, dtype=torch.complex64
150
+ )
151
+ _ops.fused_legendre_inverse(grad_input, grad_output, weight_t)
152
+ return grad_input, None
153
+
154
+
155
+ class _FusedLegendreInverseFn(torch.autograd.Function):
156
+ @staticmethod
157
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
158
+ input_c = input.contiguous()
159
+ output = torch.empty(
160
+ input_c.size(0), weight_t.size(1), input_c.size(2),
161
+ device=input_c.device, dtype=torch.complex64
162
+ )
163
+ _ops.fused_legendre_inverse(output, input_c, weight_t)
164
+ ctx.save_for_backward(weight_t)
165
+ return output
166
+
167
+ @staticmethod
168
+ def backward(ctx, grad_output: torch.Tensor):
169
+ (weight_t,) = ctx.saved_tensors
170
+ grad_output = grad_output.contiguous()
171
+ grad_input = torch.empty(
172
+ grad_output.size(0), weight_t.size(0), grad_output.size(2),
173
+ device=grad_output.device, dtype=torch.complex64
174
+ )
175
+ _ops.fused_legendre_forward(grad_input, grad_output, weight_t)
176
+ return grad_input, None
177
+
178
+
179
+ class _FusedLegendreForwardRealFn(torch.autograd.Function):
180
+ @staticmethod
181
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
182
+ input_c = input.contiguous()
183
+ output = torch.empty(
184
+ input_c.size(0), weight_t.size(0), input_c.size(2),
185
+ device=input_c.device, dtype=torch.float32
186
+ )
187
+ _ops.fused_legendre_forward_real(output, input_c, weight_t)
188
+ ctx.save_for_backward(weight_t)
189
+ return output
190
+
191
+ @staticmethod
192
+ def backward(ctx, grad_output: torch.Tensor):
193
+ (weight_t,) = ctx.saved_tensors
194
+ grad_output = grad_output.contiguous()
195
+ grad_input = torch.empty(
196
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
197
+ device=grad_output.device, dtype=torch.float32
198
+ )
199
+ _ops.fused_legendre_inverse_real(grad_input, grad_output, weight_t)
200
+ return grad_input, None
201
+
202
+
203
+ class _FusedVectorLegendreForwardFn(torch.autograd.Function):
204
+ @staticmethod
205
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
206
+ input_c = input.contiguous()
207
+ output = torch.empty(
208
+ input_c.size(0), 2, weight0_t.size(0), input_c.size(3),
209
+ device=input_c.device, dtype=torch.complex64
210
+ )
211
+ _ops.fused_vector_legendre_forward(output, input_c, weight0_t, weight1_t)
212
+ ctx.save_for_backward(weight0_t, weight1_t)
213
+ return output
214
+
215
+ @staticmethod
216
+ def backward(ctx, grad_output: torch.Tensor):
217
+ weight0_t, weight1_t = ctx.saved_tensors
218
+ grad_output = grad_output.contiguous()
219
+ grad_input = torch.empty(
220
+ grad_output.size(0), 2, weight0_t.size(1), grad_output.size(3),
221
+ device=grad_output.device, dtype=torch.complex64
222
+ )
223
+ _ops.fused_vector_legendre_inverse(grad_input, grad_output, weight0_t, weight1_t)
224
+ return grad_input, None, None
225
+
226
+
227
+ class _FusedVectorLegendreInverseFn(torch.autograd.Function):
228
+ @staticmethod
229
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
230
+ input_c = input.contiguous()
231
+ output = torch.empty(
232
+ input_c.size(0), 2, weight0_t.size(1), input_c.size(3),
233
+ device=input_c.device, dtype=torch.complex64
234
+ )
235
+ _ops.fused_vector_legendre_inverse(output, input_c, weight0_t, weight1_t)
236
+ ctx.save_for_backward(weight0_t, weight1_t)
237
+ return output
238
+
239
+ @staticmethod
240
+ def backward(ctx, grad_output: torch.Tensor):
241
+ weight0_t, weight1_t = ctx.saved_tensors
242
+ grad_output = grad_output.contiguous()
243
+ grad_input = torch.empty(
244
+ grad_output.size(0), 2, weight0_t.size(0), grad_output.size(3),
245
+ device=grad_output.device, dtype=torch.complex64
246
+ )
247
+ _ops.fused_vector_legendre_forward(grad_input, grad_output, weight0_t, weight1_t)
248
+ return grad_input, None, None
249
+
250
+
251
+ # ============================================================================
252
+ # Fused Legendre Transform
253
+ # ============================================================================
254
+
255
+ def fused_legendre_forward(
256
+ input: torch.Tensor, # [B, nlat, mmax] complex64
257
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32 (original torch-harmonics layout)
258
+ weight_t: Optional[torch.Tensor] = None, # [lmax, nlat, mmax] pre-transposed
259
+ ) -> torch.Tensor:
260
+ """Fused forward Legendre transform operating on complex tensors.
261
+
262
+ Computes out[b,l,m] = Σ_k weights[m,l,k] · input[b,k,m] for complex input,
263
+ fusing the real and imaginary multiplications into a single pass.
264
+
265
+ Uses the custom CUDA kernels when the extension is available, otherwise
266
+ falls back to a stacked einsum.
267
+ """
268
+ B = input.size(0)
269
+ nlat = input.size(1)
270
+ mmax = input.size(2)
271
+ lmax = weights.size(1)
272
+
273
+ if weight_t is None:
274
+ weight_t = weights.float().permute(1, 2, 0).contiguous()
275
+
276
+ if _can_use_cuda_legendre(input, weight_t):
277
+ # Adaptive CUDA kernel: small-grid direct path + large-grid tiled path.
278
+ return _FusedLegendreForwardFn.apply(input, weight_t)
279
+ else:
280
+ # Fallback: stacked einsum (1.9x speedup over reference 2x einsum)
281
+ x = torch.view_as_real(input) # [B, nlat, mmax, 2]
282
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0) # [2B, nlat, mmax]
283
+ w = weights.to(x_stacked.dtype)
284
+ out_stacked = torch.einsum("bkm,mlk->blm", x_stacked, w)
285
+ out_re = out_stacked[:B]
286
+ out_im = out_stacked[B:]
287
+ return torch.complex(out_re, out_im)
288
+
289
+
290
+ def fused_legendre_inverse(
291
+ input: torch.Tensor, # [B, lmax, mmax] complex64
292
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
293
+ pct_t: Optional[torch.Tensor] = None,
294
+ ) -> torch.Tensor:
295
+ """Fused inverse Legendre transform."""
296
+ B = input.size(0)
297
+ lmax = input.size(1)
298
+ mmax = input.size(2)
299
+ nlat = pct.size(2)
300
+
301
+ if pct_t is None:
302
+ pct_t = pct.float().permute(1, 2, 0).contiguous()
303
+
304
+ if _can_use_cuda_legendre(input, pct_t):
305
+ return _FusedLegendreInverseFn.apply(input, pct_t)
306
+ else:
307
+ x = torch.view_as_real(input)
308
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0)
309
+ p = pct.to(x_stacked.dtype)
310
+ out_stacked = torch.einsum("blm,mlk->bkm", x_stacked, p)
311
+ return torch.complex(out_stacked[:B], out_stacked[B:])
312
+
313
+
314
+ def fused_legendre_forward_real(
315
+ input: torch.Tensor, # [B, nlat, mmax] float32 or bfloat16
316
+ weight_t: torch.Tensor, # [lmax, nlat, mmax] float32
317
+ ) -> torch.Tensor:
318
+ """Real-valued forward Legendre transform with float accumulation."""
319
+ if _can_use_cuda_real_legendre(input, weight_t):
320
+ return _FusedLegendreForwardRealFn.apply(input, weight_t)
321
+
322
+ return torch.einsum("bkm,lkm->blm", input.float(), weight_t)
323
+
324
+
325
+ # ============================================================================
326
+ # Fused SHT (complete pipeline)
327
+ # ============================================================================
328
+
329
+ def fused_sht_forward(
330
+ x: torch.Tensor, # [B, nlat, nlon] float32
331
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32
332
+ mmax: int,
333
+ weight_t: Optional[torch.Tensor] = None,
334
+ ) -> torch.Tensor:
335
+ """Complete fused forward SHT: rfft → fused Legendre → complex coefficients.
336
+
337
+ Replaces RealSHT.forward() with fewer intermediate allocations.
338
+ """
339
+ with _nvtx_range("holysht.scalar_forward"):
340
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
341
+ x_fft = x_fft[..., :mmax]
342
+ return fused_legendre_forward(x_fft, weights, weight_t)
343
+
344
+
345
+ def fused_sht_inverse(
346
+ coeffs: torch.Tensor, # [B, lmax, mmax] complex64
347
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
348
+ nlon: int,
349
+ pct_t: Optional[torch.Tensor] = None,
350
+ ) -> torch.Tensor:
351
+ """Complete fused inverse SHT: fused Legendre → zero-pad → irfft."""
352
+ with _nvtx_range("holysht.scalar_inverse"):
353
+ x = fused_legendre_inverse(coeffs, pct, pct_t)
354
+ x = _prepare_irfft_input(x, nlon, coeffs.size(-1))
355
+ return torch.fft.irfft(x, n=nlon, dim=-1, norm="forward")
356
+
357
+
358
+ # ============================================================================
359
+ # nn.Module wrappers (drop-in replacements for torch-harmonics)
360
+ # ============================================================================
361
+
362
+ class RealSHT(nn.Module):
363
+ """Optimised drop-in replacement for ``torch_harmonics.RealSHT``.
364
+
365
+ Args:
366
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ nlat: int,
372
+ nlon: int,
373
+ lmax: Optional[int] = None,
374
+ mmax: Optional[int] = None,
375
+ grid: str = "equiangular",
376
+ norm: str = "ortho",
377
+ csphase: bool = True,
378
+ dtype: str = "fp32",
379
+ ):
380
+ super().__init__()
381
+ from torch_harmonics import RealSHT
382
+ ref = RealSHT(
383
+ nlat,
384
+ nlon,
385
+ lmax=lmax,
386
+ mmax=mmax,
387
+ grid=grid,
388
+ norm=norm,
389
+ csphase=csphase,
390
+ )
391
+ self.nlat = ref.nlat
392
+ self.nlon = ref.nlon
393
+ self.lmax = ref.lmax
394
+ self.mmax = ref.mmax
395
+ self.grid = grid
396
+ self.norm = norm
397
+ self.csphase = csphase
398
+ self._use_bf16 = (dtype == "bf16")
399
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
400
+ self.register_buffer("weights", ref.weights.to(w_dtype))
401
+ self.register_buffer("weight_t", ref.weights.float().permute(1, 2, 0).contiguous())
402
+
403
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
404
+ if self._use_bf16:
405
+ with _nvtx_range("holysht.scalar_forward_bf16"):
406
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
407
+ x_fft = x_fft[..., :self.mmax]
408
+ xr = torch.view_as_real(x_fft)
409
+ if _HAS_CUDA_EXT and x.is_cuda and not x.requires_grad:
410
+ xr_bf16 = xr.bfloat16().contiguous()
411
+ out_re = fused_legendre_forward_real(xr_bf16[..., 0].contiguous(), self.weight_t)
412
+ out_im = fused_legendre_forward_real(xr_bf16[..., 1].contiguous(), self.weight_t)
413
+ return torch.complex(out_re, out_im)
414
+
415
+ B = x.size(0)
416
+ xs = torch.cat([xr[..., 0], xr[..., 1]], dim=0).bfloat16()
417
+ out = torch.einsum("bkm,mlk->blm", xs, self.weights).float()
418
+ return torch.complex(out[:B], out[B:])
419
+ return fused_sht_forward(x, self.weights, self.mmax, self.weight_t)
420
+
421
+
422
+ class InverseRealSHT(nn.Module):
423
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealSHT``."""
424
+
425
+ def __init__(
426
+ self,
427
+ nlat: int,
428
+ nlon: int,
429
+ lmax: Optional[int] = None,
430
+ mmax: Optional[int] = None,
431
+ grid: str = "equiangular",
432
+ norm: str = "ortho",
433
+ csphase: bool = True,
434
+ ):
435
+ super().__init__()
436
+ from torch_harmonics import InverseRealSHT
437
+ ref = InverseRealSHT(
438
+ nlat,
439
+ nlon,
440
+ lmax=lmax,
441
+ mmax=mmax,
442
+ grid=grid,
443
+ norm=norm,
444
+ csphase=csphase,
445
+ )
446
+ self.nlat = ref.nlat
447
+ self.nlon = ref.nlon
448
+ self.lmax = ref.lmax
449
+ self.mmax = ref.mmax
450
+ self.grid = grid
451
+ self.norm = norm
452
+ self.csphase = csphase
453
+ self.register_buffer("pct", ref.pct.float())
454
+ self.register_buffer("pct_t", ref.pct.float().permute(1, 2, 0).contiguous())
455
+
456
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
457
+ return fused_sht_inverse(x, self.pct, self.nlon, self.pct_t)
458
+
459
+
460
+ class RealVectorSHT(nn.Module):
461
+ """Optimised drop-in replacement for ``torch_harmonics.RealVectorSHT``.
462
+
463
+ Reduces eight reference einsums to two composed Legendre passes on the
464
+ default FP32 CUDA path.
465
+
466
+ Args:
467
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
468
+ """
469
+
470
+ def __init__(
471
+ self,
472
+ nlat: int,
473
+ nlon: int,
474
+ lmax: Optional[int] = None,
475
+ mmax: Optional[int] = None,
476
+ grid: str = "equiangular",
477
+ norm: str = "ortho",
478
+ csphase: bool = True,
479
+ dtype: str = "fp32",
480
+ ):
481
+ super().__init__()
482
+ from torch_harmonics import RealVectorSHT
483
+ ref = RealVectorSHT(
484
+ nlat,
485
+ nlon,
486
+ lmax=lmax,
487
+ mmax=mmax,
488
+ grid=grid,
489
+ norm=norm,
490
+ csphase=csphase,
491
+ )
492
+ self.nlat = ref.nlat
493
+ self.nlon = ref.nlon
494
+ self.lmax = ref.lmax
495
+ self.mmax = ref.mmax
496
+ self.grid = grid
497
+ self.norm = norm
498
+ self.csphase = csphase
499
+ self._use_bf16 = (dtype == "bf16")
500
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
501
+ self.register_buffer("w0", ref.weights[0].to(w_dtype)) # [mmax, lmax, nlat]
502
+ self.register_buffer("w1", ref.weights[1].to(w_dtype)) # [mmax, lmax, nlat]
503
+ self.register_buffer("w0_t", ref.weights[0].float().permute(1, 2, 0).contiguous())
504
+ self.register_buffer("w1_t", ref.weights[1].float().permute(1, 2, 0).contiguous())
505
+
506
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
507
+ assert x.shape[-2] == self.nlat and x.shape[-1] == self.nlon
508
+
509
+ with _nvtx_range("holysht.vector_forward"):
510
+ x = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
511
+ mmax = self.mmax
512
+ x = x[..., :mmax].contiguous()
513
+
514
+ if (not self._use_bf16) and _can_use_cuda_vector(x, self.w0_t, self.w1_t):
515
+ B_shape = x.shape[:-3]
516
+ x_flat = x.reshape(-1, 2, self.nlat, mmax).contiguous()
517
+ out = _FusedVectorLegendreForwardFn.apply(x_flat, self.w0_t, self.w1_t)
518
+ return out.reshape(B_shape + (2, self.lmax, mmax))
519
+
520
+ x = torch.view_as_real(x) # [..., 2, nlat, mmax, 2]
521
+
522
+ x00 = x[..., 0, :, :, 0]
523
+ x01 = x[..., 0, :, :, 1]
524
+ x10 = x[..., 1, :, :, 0]
525
+ x11 = x[..., 1, :, :, 1]
526
+
527
+ B_shape = x00.shape[:-2]
528
+ x00_flat = x00.reshape(-1, self.nlat, mmax)
529
+ x01_flat = x01.reshape(-1, self.nlat, mmax)
530
+ x10_flat = x10.reshape(-1, self.nlat, mmax)
531
+ x11_flat = x11.reshape(-1, self.nlat, mmax)
532
+ B = x00_flat.shape[0]
533
+
534
+ if self._use_bf16 and _HAS_CUDA_EXT and x00_flat.is_cuda and not x.requires_grad:
535
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0).bfloat16().contiguous()
536
+ out_w0 = fused_legendre_forward_real(stacked_w0, self.w0_t)
537
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
538
+
539
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0).bfloat16().contiguous()
540
+ out_w1 = fused_legendre_forward_real(stacked_w1, self.w1_t)
541
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
542
+ else:
543
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
544
+ if self._use_bf16:
545
+ stacked_w0 = stacked_w0.bfloat16()
546
+ out_w0 = torch.einsum("bkm,mlk->blm", stacked_w0, self.w0)
547
+ if self._use_bf16:
548
+ out_w0 = out_w0.float()
549
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
550
+
551
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
552
+ if self._use_bf16:
553
+ stacked_w1 = stacked_w1.bfloat16()
554
+ out_w1 = torch.einsum("bkm,mlk->blm", stacked_w1, self.w1)
555
+ if self._use_bf16:
556
+ out_w1 = out_w1.float()
557
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
558
+
559
+ sph_re = r00 - s11
560
+ sph_im = r01 + s10
561
+ tor_re = -s01 - r10
562
+ tor_im = s00 - r11
563
+
564
+ out_shape = list(B_shape) + [2, self.lmax, mmax, 2]
565
+ xout = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
566
+ xout[..., 0, :, :, 0] = sph_re.reshape(B_shape + (self.lmax, mmax))
567
+ xout[..., 0, :, :, 1] = sph_im.reshape(B_shape + (self.lmax, mmax))
568
+ xout[..., 1, :, :, 0] = tor_re.reshape(B_shape + (self.lmax, mmax))
569
+ xout[..., 1, :, :, 1] = tor_im.reshape(B_shape + (self.lmax, mmax))
570
+ return torch.view_as_complex(xout)
571
+
572
+
573
+ class InverseRealVectorSHT(nn.Module):
574
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealVectorSHT``."""
575
+
576
+ def __init__(
577
+ self,
578
+ nlat: int,
579
+ nlon: int,
580
+ lmax: Optional[int] = None,
581
+ mmax: Optional[int] = None,
582
+ grid: str = "equiangular",
583
+ norm: str = "ortho",
584
+ csphase: bool = True,
585
+ ):
586
+ super().__init__()
587
+ from torch_harmonics import InverseRealVectorSHT
588
+ ref = InverseRealVectorSHT(
589
+ nlat,
590
+ nlon,
591
+ lmax=lmax,
592
+ mmax=mmax,
593
+ grid=grid,
594
+ norm=norm,
595
+ csphase=csphase,
596
+ )
597
+ self.nlat = ref.nlat
598
+ self.nlon = ref.nlon
599
+ self.lmax = ref.lmax
600
+ self.mmax = ref.mmax
601
+ self.grid = grid
602
+ self.norm = norm
603
+ self.csphase = csphase
604
+ self.register_buffer("d0", ref.dpct[0].float()) # [mmax, lmax, nlat]
605
+ self.register_buffer("d1", ref.dpct[1].float()) # [mmax, lmax, nlat]
606
+ self.register_buffer("d0_t", ref.dpct[0].float().permute(1, 2, 0).contiguous())
607
+ self.register_buffer("d1_t", ref.dpct[1].float().permute(1, 2, 0).contiguous())
608
+
609
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
610
+ assert x.shape[-2] == self.lmax and x.shape[-1] == self.mmax
611
+
612
+ with _nvtx_range("holysht.vector_inverse"):
613
+ x = x.contiguous()
614
+ if _can_use_cuda_vector(x, self.d0_t, self.d1_t):
615
+ B_shape = x.shape[:-3]
616
+ x_flat = x.reshape(-1, 2, self.lmax, self.mmax).contiguous()
617
+ x_out = _FusedVectorLegendreInverseFn.apply(x_flat, self.d0_t, self.d1_t)
618
+ x_out = x_out.reshape(B_shape + (2, self.nlat, self.mmax))
619
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
620
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
621
+
622
+ x = torch.view_as_real(x) # [..., 2, lmax, mmax, 2]
623
+ mmax = self.mmax
624
+
625
+ x00 = x[..., 0, :, :, 0]
626
+ x01 = x[..., 0, :, :, 1]
627
+ x10 = x[..., 1, :, :, 0]
628
+ x11 = x[..., 1, :, :, 1]
629
+
630
+ B_shape = x00.shape[:-2]
631
+ x00_flat = x00.reshape(-1, self.lmax, mmax)
632
+ x01_flat = x01.reshape(-1, self.lmax, mmax)
633
+ x10_flat = x10.reshape(-1, self.lmax, mmax)
634
+ x11_flat = x11.reshape(-1, self.lmax, mmax)
635
+ B = x00_flat.shape[0]
636
+
637
+ stacked_d0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
638
+ out_d0 = torch.einsum("blm,mlk->bkm", stacked_d0, self.d0)
639
+ r00, r01, r10, r11 = out_d0[:B], out_d0[B:2 * B], out_d0[2 * B:3 * B], out_d0[3 * B:]
640
+
641
+ stacked_d1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
642
+ out_d1 = torch.einsum("blm,mlk->bkm", stacked_d1, self.d1)
643
+ s11, s10, s01, s00 = out_d1[:B], out_d1[B:2 * B], out_d1[2 * B:3 * B], out_d1[3 * B:]
644
+
645
+ srl = r00 - s11
646
+ sim = r01 + s10
647
+ trl = -s01 - r10
648
+ tim = s00 - r11
649
+
650
+ out_k = self.nlat
651
+ srl = srl.reshape(B_shape + (out_k, mmax))
652
+ sim = sim.reshape(B_shape + (out_k, mmax))
653
+ trl = trl.reshape(B_shape + (out_k, mmax))
654
+ tim = tim.reshape(B_shape + (out_k, mmax))
655
+
656
+ s = torch.stack((srl, sim), -1)
657
+ t = torch.stack((trl, tim), -1)
658
+ xs = torch.stack((s, t), -4)
659
+ x_out = torch.view_as_complex(xs)
660
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
661
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
662
+
663
+
664
+ legendre_forward = fused_legendre_forward
665
+ legendre_inverse = fused_legendre_inverse
666
+ sht_forward = fused_sht_forward
667
+ sht_inverse = fused_sht_inverse
668
+
669
+ # Backwards-compatible aliases from the research prototype.
670
+ FusedRealSHT = RealSHT
671
+ FusedInverseRealSHT = InverseRealSHT
672
+ FusedRealVectorSHT = RealVectorSHT
673
+ FusedInverseRealVectorSHT = InverseRealVectorSHT
build/torch211-cxx11-cu130-x86_64-linux/_holysht_cuda_53048d0.abi3.so ADDED
Binary file (132 Bytes). View file
 
build/torch211-cxx11-cu130-x86_64-linux/_jit_ops.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local CUDA extension loader for HOLYSHT.
2
+
3
+ This keeps development off the heavyweight `kernel-builder` path by compiling a
4
+ small torch extension for the current machine only. The compiled artefacts are
5
+ cached under `build/torch_extensions`.
6
+
7
+ Author: Chris von Csefalvay
8
+ Licence: MIT
9
+ Repository: https://github.com/chrisvoncsefalvay/holysht
10
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from torch.utils.cpp_extension import load
20
+
21
+
22
+ _ROOT = Path(__file__).resolve().parents[2]
23
+ _BUILD_DIR = _ROOT / "build" / "torch_extensions"
24
+ _EXTENSION_NAME = "holysht_ops"
25
+
26
+
27
+ def _default_arch_list() -> str:
28
+ if not torch.cuda.is_available():
29
+ return ""
30
+
31
+ major, minor = torch.cuda.get_device_capability(0)
32
+ if (major, minor) >= (12, 1):
33
+ return "12.0+PTX"
34
+ return f"{major}.{minor}"
35
+
36
+
37
+ def _cuda_flags() -> list[str]:
38
+ flags = [
39
+ "-O3",
40
+ "-lineinfo",
41
+ "-Xptxas=-warn-spills",
42
+ "--expt-relaxed-constexpr",
43
+ ]
44
+ if os.environ.get("HOLYSHT_USE_FAST_MATH", "1") != "0":
45
+ flags.append("--use_fast_math")
46
+ return flags
47
+
48
+
49
+ def _load_ops():
50
+ os.environ.setdefault("MAX_JOBS", "1")
51
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _default_arch_list())
52
+ _BUILD_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ load(
55
+ name=_EXTENSION_NAME,
56
+ sources=[
57
+ str(_ROOT / "torch-ext" / "torch_binding.cpp"),
58
+ str(_ROOT / "cuda" / "fused_legendre.cu"),
59
+ str(_ROOT / "cuda" / "fused_sht.cu"),
60
+ ],
61
+ extra_include_paths=[str(_ROOT / "torch-ext")],
62
+ extra_cflags=["-O3", "-std=c++17"],
63
+ extra_cuda_cflags=_cuda_flags(),
64
+ build_directory=str(_BUILD_DIR),
65
+ verbose=os.environ.get("HOLYSHT_VERBOSE_BUILD", "0") == "1",
66
+ with_cuda=True,
67
+ is_python_module=False,
68
+ )
69
+ return getattr(torch.ops, _EXTENSION_NAME)
70
+
71
+
72
+ ops = _load_ops()
build/torch211-cxx11-cu130-x86_64-linux/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _holysht_cuda_53048d0
3
+ ops = torch.ops._holysht_cuda_53048d0
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_holysht_cuda_53048d0::{op_name}"
build/torch211-cxx11-cu130-x86_64-linux/holysht/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch211-cxx11-cu130-x86_64-linux/metadata.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "license": "mit",
4
+ "upstream": "https://github.com/NVIDIA/torch-harmonics",
5
+ "python-depends": [],
6
+ "backend": {
7
+ "type": "cuda",
8
+ "archs": [
9
+ "12.0"
10
+ ]
11
+ }
12
+ }
build/torch29-cxx11-cu129-x86_64-linux/__init__.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HOLYSHT: Highly Optimised Legendre/Ylm/SHT.
2
+
3
+ CUDA-accelerated spherical harmonic transforms designed as a practical,
4
+ production-oriented companion to torch-harmonics.
5
+
6
+ Author: Chris von Csefalvay
7
+ Licence: MIT
8
+ Repository: https://github.com/chrisvoncsefalvay/holysht
9
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
10
+ """
11
+
12
+ import contextlib
13
+ import os
14
+ from typing import Optional
15
+ import torch
16
+ import torch.nn as nn
17
+
18
+ __all__ = [
19
+ "RealSHT",
20
+ "InverseRealSHT",
21
+ "RealVectorSHT",
22
+ "InverseRealVectorSHT",
23
+ "legendre_forward",
24
+ "legendre_inverse",
25
+ "sht_forward",
26
+ "sht_inverse",
27
+ ]
28
+
29
+ # Prefer kernel-builder's generated alias module on packaged builds, then fall
30
+ # back to the local single-machine JIT loader for development.
31
+ try:
32
+ from ._ops import ops as _ops
33
+ _HAS_CUDA_EXT = True
34
+ except ModuleNotFoundError:
35
+ try:
36
+ from ._jit_ops import ops as _ops
37
+ _HAS_CUDA_EXT = True
38
+ except ImportError:
39
+ _HAS_CUDA_EXT = False
40
+ except ImportError:
41
+ _HAS_CUDA_EXT = False
42
+
43
+
44
+ def _can_use_cuda_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
45
+ return (
46
+ _HAS_CUDA_EXT
47
+ and weight_t is not None
48
+ and input.is_cuda
49
+ and weight_t.is_cuda
50
+ and input.dtype == torch.complex64
51
+ and weight_t.dtype == torch.float32
52
+ and weight_t.is_contiguous()
53
+ )
54
+
55
+
56
+ def _can_use_cuda_real_legendre(input: torch.Tensor, weight_t: Optional[torch.Tensor]) -> bool:
57
+ return (
58
+ _HAS_CUDA_EXT
59
+ and weight_t is not None
60
+ and input.is_cuda
61
+ and weight_t.is_cuda
62
+ and input.dtype in (torch.float32, torch.bfloat16)
63
+ and weight_t.dtype == torch.float32
64
+ and input.is_contiguous()
65
+ and weight_t.is_contiguous()
66
+ )
67
+
68
+
69
+ def _can_use_cuda_vector(input: torch.Tensor, weight0_t: Optional[torch.Tensor], weight1_t: Optional[torch.Tensor]) -> bool:
70
+ return (
71
+ _HAS_CUDA_EXT
72
+ and weight0_t is not None
73
+ and weight1_t is not None
74
+ and input.is_cuda
75
+ and weight0_t.is_cuda
76
+ and weight1_t.is_cuda
77
+ and input.dtype == torch.complex64
78
+ and weight0_t.dtype == torch.float32
79
+ and weight1_t.dtype == torch.float32
80
+ and input.is_contiguous()
81
+ and weight0_t.is_contiguous()
82
+ and weight1_t.is_contiguous()
83
+ )
84
+
85
+
86
+ def _mul_i(x: torch.Tensor) -> torch.Tensor:
87
+ """Multiply a complex tensor by +i without promoting dtype."""
88
+ return torch.complex(-x.imag, x.real)
89
+
90
+
91
+ @contextlib.contextmanager
92
+ def _nvtx_range(name: str):
93
+ enabled = os.environ.get("HOLYSHT_ENABLE_NVTX", "0") == "1"
94
+ if enabled and torch.cuda.is_available():
95
+ torch.cuda.nvtx.range_push(name)
96
+ try:
97
+ yield
98
+ finally:
99
+ torch.cuda.nvtx.range_pop()
100
+ else:
101
+ yield
102
+
103
+
104
+ def _prepare_irfft_input(x: torch.Tensor, nlon: int, active_mmax: Optional[int] = None) -> torch.Tensor:
105
+ """Pad/clean an rFFT-format complex tensor before irfft."""
106
+ active_mmax = x.size(-1) if active_mmax is None else active_mmax
107
+ full_mmax = nlon // 2 + 1
108
+
109
+ if x.size(-1) == full_mmax:
110
+ out = x.contiguous()
111
+ else:
112
+ out_shape = list(x.shape)
113
+ out_shape[-1] = full_mmax
114
+ out = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
115
+ out[..., :x.size(-1)] = x
116
+
117
+ if _HAS_CUDA_EXT and out.is_cuda and out.dtype == torch.complex64:
118
+ orig_shape = out.shape
119
+ flat = out.reshape(-1, orig_shape[-2], orig_shape[-1]).contiguous()
120
+ _ops.sht_prepare_irfft(flat, active_mmax, nlon)
121
+ return flat.reshape(orig_shape)
122
+
123
+ out[..., 0] = out[..., 0].real.to(torch.complex64)
124
+ if nlon % 2 == 0:
125
+ nyquist_idx = nlon // 2
126
+ if nyquist_idx < out.size(-1):
127
+ out[..., nyquist_idx] = out[..., nyquist_idx].real.to(torch.complex64)
128
+ return out
129
+
130
+
131
+ class _FusedLegendreForwardFn(torch.autograd.Function):
132
+ @staticmethod
133
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
134
+ input_c = input.contiguous()
135
+ output = torch.empty(
136
+ input_c.size(0), weight_t.size(0), input_c.size(2),
137
+ device=input_c.device, dtype=torch.complex64
138
+ )
139
+ _ops.fused_legendre_forward(output, input_c, weight_t)
140
+ ctx.save_for_backward(weight_t)
141
+ return output
142
+
143
+ @staticmethod
144
+ def backward(ctx, grad_output: torch.Tensor):
145
+ (weight_t,) = ctx.saved_tensors
146
+ grad_output = grad_output.contiguous()
147
+ grad_input = torch.empty(
148
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
149
+ device=grad_output.device, dtype=torch.complex64
150
+ )
151
+ _ops.fused_legendre_inverse(grad_input, grad_output, weight_t)
152
+ return grad_input, None
153
+
154
+
155
+ class _FusedLegendreInverseFn(torch.autograd.Function):
156
+ @staticmethod
157
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
158
+ input_c = input.contiguous()
159
+ output = torch.empty(
160
+ input_c.size(0), weight_t.size(1), input_c.size(2),
161
+ device=input_c.device, dtype=torch.complex64
162
+ )
163
+ _ops.fused_legendre_inverse(output, input_c, weight_t)
164
+ ctx.save_for_backward(weight_t)
165
+ return output
166
+
167
+ @staticmethod
168
+ def backward(ctx, grad_output: torch.Tensor):
169
+ (weight_t,) = ctx.saved_tensors
170
+ grad_output = grad_output.contiguous()
171
+ grad_input = torch.empty(
172
+ grad_output.size(0), weight_t.size(0), grad_output.size(2),
173
+ device=grad_output.device, dtype=torch.complex64
174
+ )
175
+ _ops.fused_legendre_forward(grad_input, grad_output, weight_t)
176
+ return grad_input, None
177
+
178
+
179
+ class _FusedLegendreForwardRealFn(torch.autograd.Function):
180
+ @staticmethod
181
+ def forward(ctx, input: torch.Tensor, weight_t: torch.Tensor) -> torch.Tensor:
182
+ input_c = input.contiguous()
183
+ output = torch.empty(
184
+ input_c.size(0), weight_t.size(0), input_c.size(2),
185
+ device=input_c.device, dtype=torch.float32
186
+ )
187
+ _ops.fused_legendre_forward_real(output, input_c, weight_t)
188
+ ctx.save_for_backward(weight_t)
189
+ return output
190
+
191
+ @staticmethod
192
+ def backward(ctx, grad_output: torch.Tensor):
193
+ (weight_t,) = ctx.saved_tensors
194
+ grad_output = grad_output.contiguous()
195
+ grad_input = torch.empty(
196
+ grad_output.size(0), weight_t.size(1), grad_output.size(2),
197
+ device=grad_output.device, dtype=torch.float32
198
+ )
199
+ _ops.fused_legendre_inverse_real(grad_input, grad_output, weight_t)
200
+ return grad_input, None
201
+
202
+
203
+ class _FusedVectorLegendreForwardFn(torch.autograd.Function):
204
+ @staticmethod
205
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
206
+ input_c = input.contiguous()
207
+ output = torch.empty(
208
+ input_c.size(0), 2, weight0_t.size(0), input_c.size(3),
209
+ device=input_c.device, dtype=torch.complex64
210
+ )
211
+ _ops.fused_vector_legendre_forward(output, input_c, weight0_t, weight1_t)
212
+ ctx.save_for_backward(weight0_t, weight1_t)
213
+ return output
214
+
215
+ @staticmethod
216
+ def backward(ctx, grad_output: torch.Tensor):
217
+ weight0_t, weight1_t = ctx.saved_tensors
218
+ grad_output = grad_output.contiguous()
219
+ grad_input = torch.empty(
220
+ grad_output.size(0), 2, weight0_t.size(1), grad_output.size(3),
221
+ device=grad_output.device, dtype=torch.complex64
222
+ )
223
+ _ops.fused_vector_legendre_inverse(grad_input, grad_output, weight0_t, weight1_t)
224
+ return grad_input, None, None
225
+
226
+
227
+ class _FusedVectorLegendreInverseFn(torch.autograd.Function):
228
+ @staticmethod
229
+ def forward(ctx, input: torch.Tensor, weight0_t: torch.Tensor, weight1_t: torch.Tensor) -> torch.Tensor:
230
+ input_c = input.contiguous()
231
+ output = torch.empty(
232
+ input_c.size(0), 2, weight0_t.size(1), input_c.size(3),
233
+ device=input_c.device, dtype=torch.complex64
234
+ )
235
+ _ops.fused_vector_legendre_inverse(output, input_c, weight0_t, weight1_t)
236
+ ctx.save_for_backward(weight0_t, weight1_t)
237
+ return output
238
+
239
+ @staticmethod
240
+ def backward(ctx, grad_output: torch.Tensor):
241
+ weight0_t, weight1_t = ctx.saved_tensors
242
+ grad_output = grad_output.contiguous()
243
+ grad_input = torch.empty(
244
+ grad_output.size(0), 2, weight0_t.size(0), grad_output.size(3),
245
+ device=grad_output.device, dtype=torch.complex64
246
+ )
247
+ _ops.fused_vector_legendre_forward(grad_input, grad_output, weight0_t, weight1_t)
248
+ return grad_input, None, None
249
+
250
+
251
+ # ============================================================================
252
+ # Fused Legendre Transform
253
+ # ============================================================================
254
+
255
+ def fused_legendre_forward(
256
+ input: torch.Tensor, # [B, nlat, mmax] complex64
257
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32 (original torch-harmonics layout)
258
+ weight_t: Optional[torch.Tensor] = None, # [lmax, nlat, mmax] pre-transposed
259
+ ) -> torch.Tensor:
260
+ """Fused forward Legendre transform operating on complex tensors.
261
+
262
+ Computes out[b,l,m] = Σ_k weights[m,l,k] · input[b,k,m] for complex input,
263
+ fusing the real and imaginary multiplications into a single pass.
264
+
265
+ Uses the custom CUDA kernels when the extension is available, otherwise
266
+ falls back to a stacked einsum.
267
+ """
268
+ B = input.size(0)
269
+ nlat = input.size(1)
270
+ mmax = input.size(2)
271
+ lmax = weights.size(1)
272
+
273
+ if weight_t is None:
274
+ weight_t = weights.float().permute(1, 2, 0).contiguous()
275
+
276
+ if _can_use_cuda_legendre(input, weight_t):
277
+ # Adaptive CUDA kernel: small-grid direct path + large-grid tiled path.
278
+ return _FusedLegendreForwardFn.apply(input, weight_t)
279
+ else:
280
+ # Fallback: stacked einsum (1.9x speedup over reference 2x einsum)
281
+ x = torch.view_as_real(input) # [B, nlat, mmax, 2]
282
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0) # [2B, nlat, mmax]
283
+ w = weights.to(x_stacked.dtype)
284
+ out_stacked = torch.einsum("bkm,mlk->blm", x_stacked, w)
285
+ out_re = out_stacked[:B]
286
+ out_im = out_stacked[B:]
287
+ return torch.complex(out_re, out_im)
288
+
289
+
290
+ def fused_legendre_inverse(
291
+ input: torch.Tensor, # [B, lmax, mmax] complex64
292
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
293
+ pct_t: Optional[torch.Tensor] = None,
294
+ ) -> torch.Tensor:
295
+ """Fused inverse Legendre transform."""
296
+ B = input.size(0)
297
+ lmax = input.size(1)
298
+ mmax = input.size(2)
299
+ nlat = pct.size(2)
300
+
301
+ if pct_t is None:
302
+ pct_t = pct.float().permute(1, 2, 0).contiguous()
303
+
304
+ if _can_use_cuda_legendre(input, pct_t):
305
+ return _FusedLegendreInverseFn.apply(input, pct_t)
306
+ else:
307
+ x = torch.view_as_real(input)
308
+ x_stacked = torch.cat([x[..., 0], x[..., 1]], dim=0)
309
+ p = pct.to(x_stacked.dtype)
310
+ out_stacked = torch.einsum("blm,mlk->bkm", x_stacked, p)
311
+ return torch.complex(out_stacked[:B], out_stacked[B:])
312
+
313
+
314
+ def fused_legendre_forward_real(
315
+ input: torch.Tensor, # [B, nlat, mmax] float32 or bfloat16
316
+ weight_t: torch.Tensor, # [lmax, nlat, mmax] float32
317
+ ) -> torch.Tensor:
318
+ """Real-valued forward Legendre transform with float accumulation."""
319
+ if _can_use_cuda_real_legendre(input, weight_t):
320
+ return _FusedLegendreForwardRealFn.apply(input, weight_t)
321
+
322
+ return torch.einsum("bkm,lkm->blm", input.float(), weight_t)
323
+
324
+
325
+ # ============================================================================
326
+ # Fused SHT (complete pipeline)
327
+ # ============================================================================
328
+
329
+ def fused_sht_forward(
330
+ x: torch.Tensor, # [B, nlat, nlon] float32
331
+ weights: torch.Tensor, # [mmax, lmax, nlat] float32
332
+ mmax: int,
333
+ weight_t: Optional[torch.Tensor] = None,
334
+ ) -> torch.Tensor:
335
+ """Complete fused forward SHT: rfft → fused Legendre → complex coefficients.
336
+
337
+ Replaces RealSHT.forward() with fewer intermediate allocations.
338
+ """
339
+ with _nvtx_range("holysht.scalar_forward"):
340
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
341
+ x_fft = x_fft[..., :mmax]
342
+ return fused_legendre_forward(x_fft, weights, weight_t)
343
+
344
+
345
+ def fused_sht_inverse(
346
+ coeffs: torch.Tensor, # [B, lmax, mmax] complex64
347
+ pct: torch.Tensor, # [mmax, lmax, nlat] float32
348
+ nlon: int,
349
+ pct_t: Optional[torch.Tensor] = None,
350
+ ) -> torch.Tensor:
351
+ """Complete fused inverse SHT: fused Legendre → zero-pad → irfft."""
352
+ with _nvtx_range("holysht.scalar_inverse"):
353
+ x = fused_legendre_inverse(coeffs, pct, pct_t)
354
+ x = _prepare_irfft_input(x, nlon, coeffs.size(-1))
355
+ return torch.fft.irfft(x, n=nlon, dim=-1, norm="forward")
356
+
357
+
358
+ # ============================================================================
359
+ # nn.Module wrappers (drop-in replacements for torch-harmonics)
360
+ # ============================================================================
361
+
362
+ class RealSHT(nn.Module):
363
+ """Optimised drop-in replacement for ``torch_harmonics.RealSHT``.
364
+
365
+ Args:
366
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ nlat: int,
372
+ nlon: int,
373
+ lmax: Optional[int] = None,
374
+ mmax: Optional[int] = None,
375
+ grid: str = "equiangular",
376
+ norm: str = "ortho",
377
+ csphase: bool = True,
378
+ dtype: str = "fp32",
379
+ ):
380
+ super().__init__()
381
+ from torch_harmonics import RealSHT
382
+ ref = RealSHT(
383
+ nlat,
384
+ nlon,
385
+ lmax=lmax,
386
+ mmax=mmax,
387
+ grid=grid,
388
+ norm=norm,
389
+ csphase=csphase,
390
+ )
391
+ self.nlat = ref.nlat
392
+ self.nlon = ref.nlon
393
+ self.lmax = ref.lmax
394
+ self.mmax = ref.mmax
395
+ self.grid = grid
396
+ self.norm = norm
397
+ self.csphase = csphase
398
+ self._use_bf16 = (dtype == "bf16")
399
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
400
+ self.register_buffer("weights", ref.weights.to(w_dtype))
401
+ self.register_buffer("weight_t", ref.weights.float().permute(1, 2, 0).contiguous())
402
+
403
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
404
+ if self._use_bf16:
405
+ with _nvtx_range("holysht.scalar_forward_bf16"):
406
+ x_fft = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
407
+ x_fft = x_fft[..., :self.mmax]
408
+ xr = torch.view_as_real(x_fft)
409
+ if _HAS_CUDA_EXT and x.is_cuda and not x.requires_grad:
410
+ xr_bf16 = xr.bfloat16().contiguous()
411
+ out_re = fused_legendre_forward_real(xr_bf16[..., 0].contiguous(), self.weight_t)
412
+ out_im = fused_legendre_forward_real(xr_bf16[..., 1].contiguous(), self.weight_t)
413
+ return torch.complex(out_re, out_im)
414
+
415
+ B = x.size(0)
416
+ xs = torch.cat([xr[..., 0], xr[..., 1]], dim=0).bfloat16()
417
+ out = torch.einsum("bkm,mlk->blm", xs, self.weights).float()
418
+ return torch.complex(out[:B], out[B:])
419
+ return fused_sht_forward(x, self.weights, self.mmax, self.weight_t)
420
+
421
+
422
+ class InverseRealSHT(nn.Module):
423
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealSHT``."""
424
+
425
+ def __init__(
426
+ self,
427
+ nlat: int,
428
+ nlon: int,
429
+ lmax: Optional[int] = None,
430
+ mmax: Optional[int] = None,
431
+ grid: str = "equiangular",
432
+ norm: str = "ortho",
433
+ csphase: bool = True,
434
+ ):
435
+ super().__init__()
436
+ from torch_harmonics import InverseRealSHT
437
+ ref = InverseRealSHT(
438
+ nlat,
439
+ nlon,
440
+ lmax=lmax,
441
+ mmax=mmax,
442
+ grid=grid,
443
+ norm=norm,
444
+ csphase=csphase,
445
+ )
446
+ self.nlat = ref.nlat
447
+ self.nlon = ref.nlon
448
+ self.lmax = ref.lmax
449
+ self.mmax = ref.mmax
450
+ self.grid = grid
451
+ self.norm = norm
452
+ self.csphase = csphase
453
+ self.register_buffer("pct", ref.pct.float())
454
+ self.register_buffer("pct_t", ref.pct.float().permute(1, 2, 0).contiguous())
455
+
456
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
457
+ return fused_sht_inverse(x, self.pct, self.nlon, self.pct_t)
458
+
459
+
460
+ class RealVectorSHT(nn.Module):
461
+ """Optimised drop-in replacement for ``torch_harmonics.RealVectorSHT``.
462
+
463
+ Reduces eight reference einsums to two composed Legendre passes on the
464
+ default FP32 CUDA path.
465
+
466
+ Args:
467
+ dtype: Weight precision. ``"fp32"`` (default) or ``"bf16"``.
468
+ """
469
+
470
+ def __init__(
471
+ self,
472
+ nlat: int,
473
+ nlon: int,
474
+ lmax: Optional[int] = None,
475
+ mmax: Optional[int] = None,
476
+ grid: str = "equiangular",
477
+ norm: str = "ortho",
478
+ csphase: bool = True,
479
+ dtype: str = "fp32",
480
+ ):
481
+ super().__init__()
482
+ from torch_harmonics import RealVectorSHT
483
+ ref = RealVectorSHT(
484
+ nlat,
485
+ nlon,
486
+ lmax=lmax,
487
+ mmax=mmax,
488
+ grid=grid,
489
+ norm=norm,
490
+ csphase=csphase,
491
+ )
492
+ self.nlat = ref.nlat
493
+ self.nlon = ref.nlon
494
+ self.lmax = ref.lmax
495
+ self.mmax = ref.mmax
496
+ self.grid = grid
497
+ self.norm = norm
498
+ self.csphase = csphase
499
+ self._use_bf16 = (dtype == "bf16")
500
+ w_dtype = torch.bfloat16 if self._use_bf16 else torch.float32
501
+ self.register_buffer("w0", ref.weights[0].to(w_dtype)) # [mmax, lmax, nlat]
502
+ self.register_buffer("w1", ref.weights[1].to(w_dtype)) # [mmax, lmax, nlat]
503
+ self.register_buffer("w0_t", ref.weights[0].float().permute(1, 2, 0).contiguous())
504
+ self.register_buffer("w1_t", ref.weights[1].float().permute(1, 2, 0).contiguous())
505
+
506
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
507
+ assert x.shape[-2] == self.nlat and x.shape[-1] == self.nlon
508
+
509
+ with _nvtx_range("holysht.vector_forward"):
510
+ x = 2.0 * torch.pi * torch.fft.rfft(x, dim=-1, norm="forward")
511
+ mmax = self.mmax
512
+ x = x[..., :mmax].contiguous()
513
+
514
+ if (not self._use_bf16) and _can_use_cuda_vector(x, self.w0_t, self.w1_t):
515
+ B_shape = x.shape[:-3]
516
+ x_flat = x.reshape(-1, 2, self.nlat, mmax).contiguous()
517
+ out = _FusedVectorLegendreForwardFn.apply(x_flat, self.w0_t, self.w1_t)
518
+ return out.reshape(B_shape + (2, self.lmax, mmax))
519
+
520
+ x = torch.view_as_real(x) # [..., 2, nlat, mmax, 2]
521
+
522
+ x00 = x[..., 0, :, :, 0]
523
+ x01 = x[..., 0, :, :, 1]
524
+ x10 = x[..., 1, :, :, 0]
525
+ x11 = x[..., 1, :, :, 1]
526
+
527
+ B_shape = x00.shape[:-2]
528
+ x00_flat = x00.reshape(-1, self.nlat, mmax)
529
+ x01_flat = x01.reshape(-1, self.nlat, mmax)
530
+ x10_flat = x10.reshape(-1, self.nlat, mmax)
531
+ x11_flat = x11.reshape(-1, self.nlat, mmax)
532
+ B = x00_flat.shape[0]
533
+
534
+ if self._use_bf16 and _HAS_CUDA_EXT and x00_flat.is_cuda and not x.requires_grad:
535
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0).bfloat16().contiguous()
536
+ out_w0 = fused_legendre_forward_real(stacked_w0, self.w0_t)
537
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
538
+
539
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0).bfloat16().contiguous()
540
+ out_w1 = fused_legendre_forward_real(stacked_w1, self.w1_t)
541
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
542
+ else:
543
+ stacked_w0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
544
+ if self._use_bf16:
545
+ stacked_w0 = stacked_w0.bfloat16()
546
+ out_w0 = torch.einsum("bkm,mlk->blm", stacked_w0, self.w0)
547
+ if self._use_bf16:
548
+ out_w0 = out_w0.float()
549
+ r00, r01, r10, r11 = out_w0[:B], out_w0[B:2 * B], out_w0[2 * B:3 * B], out_w0[3 * B:]
550
+
551
+ stacked_w1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
552
+ if self._use_bf16:
553
+ stacked_w1 = stacked_w1.bfloat16()
554
+ out_w1 = torch.einsum("bkm,mlk->blm", stacked_w1, self.w1)
555
+ if self._use_bf16:
556
+ out_w1 = out_w1.float()
557
+ s11, s10, s01, s00 = out_w1[:B], out_w1[B:2 * B], out_w1[2 * B:3 * B], out_w1[3 * B:]
558
+
559
+ sph_re = r00 - s11
560
+ sph_im = r01 + s10
561
+ tor_re = -s01 - r10
562
+ tor_im = s00 - r11
563
+
564
+ out_shape = list(B_shape) + [2, self.lmax, mmax, 2]
565
+ xout = torch.zeros(out_shape, dtype=x.dtype, device=x.device)
566
+ xout[..., 0, :, :, 0] = sph_re.reshape(B_shape + (self.lmax, mmax))
567
+ xout[..., 0, :, :, 1] = sph_im.reshape(B_shape + (self.lmax, mmax))
568
+ xout[..., 1, :, :, 0] = tor_re.reshape(B_shape + (self.lmax, mmax))
569
+ xout[..., 1, :, :, 1] = tor_im.reshape(B_shape + (self.lmax, mmax))
570
+ return torch.view_as_complex(xout)
571
+
572
+
573
+ class InverseRealVectorSHT(nn.Module):
574
+ """Optimised drop-in replacement for ``torch_harmonics.InverseRealVectorSHT``."""
575
+
576
+ def __init__(
577
+ self,
578
+ nlat: int,
579
+ nlon: int,
580
+ lmax: Optional[int] = None,
581
+ mmax: Optional[int] = None,
582
+ grid: str = "equiangular",
583
+ norm: str = "ortho",
584
+ csphase: bool = True,
585
+ ):
586
+ super().__init__()
587
+ from torch_harmonics import InverseRealVectorSHT
588
+ ref = InverseRealVectorSHT(
589
+ nlat,
590
+ nlon,
591
+ lmax=lmax,
592
+ mmax=mmax,
593
+ grid=grid,
594
+ norm=norm,
595
+ csphase=csphase,
596
+ )
597
+ self.nlat = ref.nlat
598
+ self.nlon = ref.nlon
599
+ self.lmax = ref.lmax
600
+ self.mmax = ref.mmax
601
+ self.grid = grid
602
+ self.norm = norm
603
+ self.csphase = csphase
604
+ self.register_buffer("d0", ref.dpct[0].float()) # [mmax, lmax, nlat]
605
+ self.register_buffer("d1", ref.dpct[1].float()) # [mmax, lmax, nlat]
606
+ self.register_buffer("d0_t", ref.dpct[0].float().permute(1, 2, 0).contiguous())
607
+ self.register_buffer("d1_t", ref.dpct[1].float().permute(1, 2, 0).contiguous())
608
+
609
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
610
+ assert x.shape[-2] == self.lmax and x.shape[-1] == self.mmax
611
+
612
+ with _nvtx_range("holysht.vector_inverse"):
613
+ x = x.contiguous()
614
+ if _can_use_cuda_vector(x, self.d0_t, self.d1_t):
615
+ B_shape = x.shape[:-3]
616
+ x_flat = x.reshape(-1, 2, self.lmax, self.mmax).contiguous()
617
+ x_out = _FusedVectorLegendreInverseFn.apply(x_flat, self.d0_t, self.d1_t)
618
+ x_out = x_out.reshape(B_shape + (2, self.nlat, self.mmax))
619
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
620
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
621
+
622
+ x = torch.view_as_real(x) # [..., 2, lmax, mmax, 2]
623
+ mmax = self.mmax
624
+
625
+ x00 = x[..., 0, :, :, 0]
626
+ x01 = x[..., 0, :, :, 1]
627
+ x10 = x[..., 1, :, :, 0]
628
+ x11 = x[..., 1, :, :, 1]
629
+
630
+ B_shape = x00.shape[:-2]
631
+ x00_flat = x00.reshape(-1, self.lmax, mmax)
632
+ x01_flat = x01.reshape(-1, self.lmax, mmax)
633
+ x10_flat = x10.reshape(-1, self.lmax, mmax)
634
+ x11_flat = x11.reshape(-1, self.lmax, mmax)
635
+ B = x00_flat.shape[0]
636
+
637
+ stacked_d0 = torch.cat([x00_flat, x01_flat, x10_flat, x11_flat], dim=0)
638
+ out_d0 = torch.einsum("blm,mlk->bkm", stacked_d0, self.d0)
639
+ r00, r01, r10, r11 = out_d0[:B], out_d0[B:2 * B], out_d0[2 * B:3 * B], out_d0[3 * B:]
640
+
641
+ stacked_d1 = torch.cat([x11_flat, x10_flat, x01_flat, x00_flat], dim=0)
642
+ out_d1 = torch.einsum("blm,mlk->bkm", stacked_d1, self.d1)
643
+ s11, s10, s01, s00 = out_d1[:B], out_d1[B:2 * B], out_d1[2 * B:3 * B], out_d1[3 * B:]
644
+
645
+ srl = r00 - s11
646
+ sim = r01 + s10
647
+ trl = -s01 - r10
648
+ tim = s00 - r11
649
+
650
+ out_k = self.nlat
651
+ srl = srl.reshape(B_shape + (out_k, mmax))
652
+ sim = sim.reshape(B_shape + (out_k, mmax))
653
+ trl = trl.reshape(B_shape + (out_k, mmax))
654
+ tim = tim.reshape(B_shape + (out_k, mmax))
655
+
656
+ s = torch.stack((srl, sim), -1)
657
+ t = torch.stack((trl, tim), -1)
658
+ xs = torch.stack((s, t), -4)
659
+ x_out = torch.view_as_complex(xs)
660
+ x_out = _prepare_irfft_input(x_out, self.nlon, self.mmax)
661
+ return torch.fft.irfft(x_out, n=self.nlon, dim=-1, norm="forward")
662
+
663
+
664
+ legendre_forward = fused_legendre_forward
665
+ legendre_inverse = fused_legendre_inverse
666
+ sht_forward = fused_sht_forward
667
+ sht_inverse = fused_sht_inverse
668
+
669
+ # Backwards-compatible aliases from the research prototype.
670
+ FusedRealSHT = RealSHT
671
+ FusedInverseRealSHT = InverseRealSHT
672
+ FusedRealVectorSHT = RealVectorSHT
673
+ FusedInverseRealVectorSHT = InverseRealVectorSHT
build/torch29-cxx11-cu129-x86_64-linux/_holysht_cuda_53048d0.abi3.so ADDED
Binary file (132 Bytes). View file
 
build/torch29-cxx11-cu129-x86_64-linux/_jit_ops.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local CUDA extension loader for HOLYSHT.
2
+
3
+ This keeps development off the heavyweight `kernel-builder` path by compiling a
4
+ small torch extension for the current machine only. The compiled artefacts are
5
+ cached under `build/torch_extensions`.
6
+
7
+ Author: Chris von Csefalvay
8
+ Licence: MIT
9
+ Repository: https://github.com/chrisvoncsefalvay/holysht
10
+ Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from torch.utils.cpp_extension import load
20
+
21
+
22
+ _ROOT = Path(__file__).resolve().parents[2]
23
+ _BUILD_DIR = _ROOT / "build" / "torch_extensions"
24
+ _EXTENSION_NAME = "holysht_ops"
25
+
26
+
27
+ def _default_arch_list() -> str:
28
+ if not torch.cuda.is_available():
29
+ return ""
30
+
31
+ major, minor = torch.cuda.get_device_capability(0)
32
+ if (major, minor) >= (12, 1):
33
+ return "12.0+PTX"
34
+ return f"{major}.{minor}"
35
+
36
+
37
+ def _cuda_flags() -> list[str]:
38
+ flags = [
39
+ "-O3",
40
+ "-lineinfo",
41
+ "-Xptxas=-warn-spills",
42
+ "--expt-relaxed-constexpr",
43
+ ]
44
+ if os.environ.get("HOLYSHT_USE_FAST_MATH", "1") != "0":
45
+ flags.append("--use_fast_math")
46
+ return flags
47
+
48
+
49
+ def _load_ops():
50
+ os.environ.setdefault("MAX_JOBS", "1")
51
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _default_arch_list())
52
+ _BUILD_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ load(
55
+ name=_EXTENSION_NAME,
56
+ sources=[
57
+ str(_ROOT / "torch-ext" / "torch_binding.cpp"),
58
+ str(_ROOT / "cuda" / "fused_legendre.cu"),
59
+ str(_ROOT / "cuda" / "fused_sht.cu"),
60
+ ],
61
+ extra_include_paths=[str(_ROOT / "torch-ext")],
62
+ extra_cflags=["-O3", "-std=c++17"],
63
+ extra_cuda_cflags=_cuda_flags(),
64
+ build_directory=str(_BUILD_DIR),
65
+ verbose=os.environ.get("HOLYSHT_VERBOSE_BUILD", "0") == "1",
66
+ with_cuda=True,
67
+ is_python_module=False,
68
+ )
69
+ return getattr(torch.ops, _EXTENSION_NAME)
70
+
71
+
72
+ ops = _load_ops()
build/torch29-cxx11-cu129-x86_64-linux/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _holysht_cuda_53048d0
3
+ ops = torch.ops._holysht_cuda_53048d0
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_holysht_cuda_53048d0::{op_name}"
build/torch29-cxx11-cu129-x86_64-linux/holysht/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch29-cxx11-cu129-x86_64-linux/metadata.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "license": "mit",
4
+ "upstream": "https://github.com/NVIDIA/torch-harmonics",
5
+ "python-depends": [],
6
+ "backend": {
7
+ "type": "cuda",
8
+ "archs": [
9
+ "12.0"
10
+ ]
11
+ }
12
+ }
flake.lock ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nodes": {
3
+ "flake-compat": {
4
+ "locked": {
5
+ "lastModified": 1767039857,
6
+ "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
7
+ "owner": "edolstra",
8
+ "repo": "flake-compat",
9
+ "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "owner": "edolstra",
14
+ "repo": "flake-compat",
15
+ "type": "github"
16
+ }
17
+ },
18
+ "flake-utils": {
19
+ "inputs": {
20
+ "systems": "systems"
21
+ },
22
+ "locked": {
23
+ "lastModified": 1731533236,
24
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
25
+ "owner": "numtide",
26
+ "repo": "flake-utils",
27
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
28
+ "type": "github"
29
+ },
30
+ "original": {
31
+ "owner": "numtide",
32
+ "repo": "flake-utils",
33
+ "type": "github"
34
+ }
35
+ },
36
+ "kernel-builder": {
37
+ "inputs": {
38
+ "flake-compat": "flake-compat",
39
+ "flake-utils": "flake-utils",
40
+ "nixpkgs": "nixpkgs",
41
+ "rust-overlay": "rust-overlay"
42
+ },
43
+ "locked": {
44
+ "lastModified": 1775826501,
45
+ "narHash": "sha256-Do1JnE2aRIPW5Y2dbgx/Wj77/0wbvuROtCFGYnDUaKY=",
46
+ "owner": "huggingface",
47
+ "repo": "kernels",
48
+ "rev": "2fe18de4da9a6769e9cc3b615aff27db5b3d21dd",
49
+ "type": "github"
50
+ },
51
+ "original": {
52
+ "owner": "huggingface",
53
+ "repo": "kernels",
54
+ "type": "github"
55
+ }
56
+ },
57
+ "nixpkgs": {
58
+ "locked": {
59
+ "lastModified": 1774935083,
60
+ "narHash": "sha256-Mh6bLcYAcENBAZk3RoMPMFCGGMZmfaGMERE4siZOgP4=",
61
+ "owner": "NixOS",
62
+ "repo": "nixpkgs",
63
+ "rev": "2f4fd5e1abf9bac8c1d22750c701a7a5e6b524c6",
64
+ "type": "github"
65
+ },
66
+ "original": {
67
+ "owner": "NixOS",
68
+ "ref": "nixos-unstable-small",
69
+ "repo": "nixpkgs",
70
+ "type": "github"
71
+ }
72
+ },
73
+ "root": {
74
+ "inputs": {
75
+ "kernel-builder": "kernel-builder"
76
+ }
77
+ },
78
+ "rust-overlay": {
79
+ "inputs": {
80
+ "nixpkgs": [
81
+ "kernel-builder",
82
+ "nixpkgs"
83
+ ]
84
+ },
85
+ "locked": {
86
+ "lastModified": 1774926780,
87
+ "narHash": "sha256-JMdDYn0F+swYBILlpCeHDbCSyzqkeSGNxZ/Q5J584jM=",
88
+ "owner": "oxalica",
89
+ "repo": "rust-overlay",
90
+ "rev": "962a0934d0e32f42d1b5e49186f9595f9b178d2d",
91
+ "type": "github"
92
+ },
93
+ "original": {
94
+ "owner": "oxalica",
95
+ "repo": "rust-overlay",
96
+ "type": "github"
97
+ }
98
+ },
99
+ "systems": {
100
+ "locked": {
101
+ "lastModified": 1681028828,
102
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
103
+ "owner": "nix-systems",
104
+ "repo": "default",
105
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
106
+ "type": "github"
107
+ },
108
+ "original": {
109
+ "owner": "nix-systems",
110
+ "repo": "default",
111
+ "type": "github"
112
+ }
113
+ }
114
+ },
115
+ "root": "root",
116
+ "version": 7
117
+ }
flake.nix ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HOLYSHT
2
+ # Author: Chris von Csefalvay
3
+ # Licence: MIT
4
+ # Repository: https://github.com/chrisvoncsefalvay/holysht
5
+ # Hugging Face kernel: https://hf.co/chrisvoncsefalvay/holysht
6
+
7
+ {
8
+ inputs = {
9
+ kernel-builder.url = "github:huggingface/kernels";
10
+ };
11
+ outputs =
12
+ { self, kernel-builder, ... }:
13
+ kernel-builder.lib.genKernelFlakeOutputs {
14
+ inherit self;
15
+ path = ./.;
16
+ };
17
+ }