Publish HOLYSHT kernel bundle (53048d01ccae)
Browse files- .gitattributes +11 -35
- LICENSE +21 -0
- README.md +91 -0
- build.toml +38 -0
- build/torch210-cxx11-cu126-x86_64-linux/__init__.py +673 -0
- build/torch210-cxx11-cu126-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
- build/torch210-cxx11-cu126-x86_64-linux/_jit_ops.py +72 -0
- build/torch210-cxx11-cu126-x86_64-linux/_ops.py +9 -0
- build/torch210-cxx11-cu126-x86_64-linux/holysht/__init__.py +26 -0
- build/torch210-cxx11-cu126-x86_64-linux/metadata.json +9 -0
- build/torch210-cxx11-cu128-x86_64-linux/__init__.py +673 -0
- build/torch210-cxx11-cu128-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
- build/torch210-cxx11-cu128-x86_64-linux/_jit_ops.py +72 -0
- build/torch210-cxx11-cu128-x86_64-linux/_ops.py +9 -0
- build/torch210-cxx11-cu128-x86_64-linux/holysht/__init__.py +26 -0
- build/torch210-cxx11-cu128-x86_64-linux/metadata.json +12 -0
- build/torch210-cxx11-cu130-x86_64-linux/__init__.py +673 -0
- build/torch210-cxx11-cu130-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
- build/torch210-cxx11-cu130-x86_64-linux/_jit_ops.py +72 -0
- build/torch210-cxx11-cu130-x86_64-linux/_ops.py +9 -0
- build/torch210-cxx11-cu130-x86_64-linux/holysht/__init__.py +26 -0
- build/torch210-cxx11-cu130-x86_64-linux/metadata.json +12 -0
- build/torch211-cxx11-cu126-x86_64-linux/__init__.py +673 -0
- build/torch211-cxx11-cu126-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
- build/torch211-cxx11-cu126-x86_64-linux/_jit_ops.py +72 -0
- build/torch211-cxx11-cu126-x86_64-linux/_ops.py +9 -0
- build/torch211-cxx11-cu126-x86_64-linux/holysht/__init__.py +26 -0
- build/torch211-cxx11-cu126-x86_64-linux/metadata.json +9 -0
- build/torch211-cxx11-cu128-x86_64-linux/__init__.py +673 -0
- build/torch211-cxx11-cu128-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
- build/torch211-cxx11-cu128-x86_64-linux/_jit_ops.py +72 -0
- build/torch211-cxx11-cu128-x86_64-linux/_ops.py +9 -0
- build/torch211-cxx11-cu128-x86_64-linux/holysht/__init__.py +26 -0
- build/torch211-cxx11-cu128-x86_64-linux/metadata.json +12 -0
- build/torch211-cxx11-cu130-x86_64-linux/__init__.py +673 -0
- build/torch211-cxx11-cu130-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
- build/torch211-cxx11-cu130-x86_64-linux/_jit_ops.py +72 -0
- build/torch211-cxx11-cu130-x86_64-linux/_ops.py +9 -0
- build/torch211-cxx11-cu130-x86_64-linux/holysht/__init__.py +26 -0
- build/torch211-cxx11-cu130-x86_64-linux/metadata.json +12 -0
- build/torch29-cxx11-cu129-x86_64-linux/__init__.py +673 -0
- build/torch29-cxx11-cu129-x86_64-linux/_holysht_cuda_53048d0.abi3.so +0 -0
- build/torch29-cxx11-cu129-x86_64-linux/_jit_ops.py +72 -0
- build/torch29-cxx11-cu129-x86_64-linux/_ops.py +9 -0
- build/torch29-cxx11-cu129-x86_64-linux/holysht/__init__.py +26 -0
- build/torch29-cxx11-cu129-x86_64-linux/metadata.json +12 -0
- flake.lock +117 -0
- flake.nix +17 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,11 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 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 |
+
}
|