Skip to content

GPU-safe constructors, custom metric signatures, and Julia 1.10 LTS#37

Open
AndersEdin wants to merge 5 commits into
brainandforce:mainfrom
AndersEdin:clifford-fixes
Open

GPU-safe constructors, custom metric signatures, and Julia 1.10 LTS#37
AndersEdin wants to merge 5 commits into
brainandforce:mainfrom
AndersEdin:clifford-fixes

Conversation

@AndersEdin

Copy link
Copy Markdown

Here comes the bug fixes and maintenance fixes from my previous PR. The issues were found when implementing a geometric algebra package using CliffordNumbers, and both the GPU fixes and the fix to enable defining metric signatures are needed for being able to use the package.

I have raised the minimum Julia to the 1.10 LTS, which makes sense since it is a lot of work to support older versions. This also aligns with what is already written in the package documentation.

Changes

Julia 1.10 LTS. Compat lower bound raised to 1.10 in the package and test environments. dimension(::Signature) now returns a full-width Int instead of Int8: on 1.10 a narrow integer sends binomial to its Float64 gamma-function method, which made nblades of a KVector in a custom-signature algebra return a Float64 and broke construction. Zero-allocation test gates are placed behind ALLOCATION_GATES = VERSION >= v"1.12"; the 1.10 compiler misses some of the constant-folds those gates assert, so they are informative-only there while correctness and inference tests run on every version.

GPU enablement. Adds an Adapt package extension so AbstractCliffordNumber values adapt to/from device arrays, plus constructor paths that allocate no boxed intermediates on the device path. The sign-changing automorphisms (reverse, grade_involution, conj) are reimplemented as branch-free bit arithmetic on the blade grade — reversion (-1)^{k(k-1)/2}, grade involution (-1)^k — so they carry no data-dependent branch and codegen uniformly on the GPU. Results are unchanged.

Custom metric signatures. metric_tuple(s) -> NTuple{N,Int8} is the single, world-age-robust source of metric data; the generated kernels and the negative/zero square-bit masks all read it. A user metric is expressed either as a Signature value in the type parameter, or as an AbstractSignature subtype reduced to a value with Signature(s) (the built-in STAP uses the same shape). A bare downstream subtype cannot drive the @generated kernels — their generators run at CliffordNumbers' load-time world age and cannot see interface methods defined later — so Signature(s), computed at the caller's world age, is the bridge. Tutorial in docs/src/custom_signatures.md.

Tests

test/custom_signature.jl exercises a phase-space (D, D, R) family: signature interface, nblades integrality on 1.10, metric_tuple as the sole metric source, the world-age failure of a bare subtype, and the Signature(s) bridge driving the generated product/automorphism/inverse kernels. GPU extension covered by test/ext/Adapt.jl. Pkg.test() passes on the 1.10 LTS.

Breaking

  • Minimum supported Julia is now 1.10.

AndersEdin and others added 5 commits July 4, 2026 17:59
Make arrays of Clifford numbers usable on the GPU.

- Constructors: replace interpolated `@assert` messages in
  `check_element_count`, the `KVector` K-range check, and the
  `Z2CliffordNumber` parity check with constant `String` literals. An
  interpolated message allocates a `String` in the throw path (via
  `print_to_string`/`apply_generic`), which GPU compilers reject
  (`jl_alloc_string` is unsupported on-device). The checks are kept; for
  concrete types they are statically true and fully eliminated, so the
  constructors emit zero throws and zero allocations.

- Adapt: add `ext/CliffordNumbersAdaptExt.jl` registering
  `Adapt.adapt_structure`. These types are `isbits` with `NTuple{L,T}`
  storage, so the only meaningful adaptation is retyping the scalar
  coefficients to match a typed-array destination (e.g.
  `adapt(Array{Float32}, ::KVector{...,Float64})` -> `...,Float32`); other
  destinations preserve the scalar type. Wired into `[weakdeps]`/
  `[extensions]`/`[compat]` and covered by `test/ext/Adapt.jl`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On-device testing (RTX 3070, CUDA.jl) showed that `reverse`, `adjoint`,
`conj`, and `grade_involution` did not lower on the GPU for `CliffordNumber`
(dense) and `Z2CliffordNumber`, and `reverse` failed for `KVector` too: the
generic fallback `T(x[f.(BladeIndices(T))])` resolves blade positions at
runtime through `to_index`, which the GPU compiler rejects (gpu_gc_pool_alloc
+ dynamic dispatch). This blocked the headline `rotate.(cu_pts, Ref(R))` /
rotor-sandwich path, since the sandwich uses `R'`.

- Replace the generic indexing loop with a `@generated` `_sign_automorphism`
  that bakes every blade position and sign at compile time, emitting pure
  tuple arithmetic (allocation-free, GPU-safe). Works for all dense / Z2 /
  user types.
- Add the closed-form `reverse(::KVector)` (identical to `adjoint` for real
  scalars), the one case the existing KVector closed forms had missed.

Verified: the four automorphisms and a rotor-sandwich broadcast now compile
and run on-device and match the CPU result. A new CPU regression testset pins
all four automorphisms against the original indexing semantics across dense /
Even / Odd / KVector over VGA(3), PGA(3), and STA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
metric_tuple(s) returns a signature's metric as an NTuple of Int8 and is
the single, world-age-robust source of metric data: the generated blade
kernels and the negative/zero square-bit masks both read it, and it works
on a downstream AbstractSignature subtype by reading its interface at the
caller's world age rather than inside a generator.

This makes user-defined signatures a supported extension path. A custom
metric is expressed either as a Signature *value* placed in the Clifford
number's type parameter, or as an AbstractSignature subtype bridged to a
value with Signature(s) (mirroring how CliffordNumbers' own STAP is
built). docs/src/custom_signatures.md walks through both, and
test/custom_signature.jl exercises a phase-space (D, D, R) family: the
signature interface, metric_tuple as the single metric source, the
world-age limitation of a bare subtype, and the Signature(s) bridge
driving the generated product/automorphism/inverse kernels end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Raise the compat lower bound to 1.10 in the package and test environments
(1.9 support was already nominal: package extensions are the only 1.9
feature in use).

dimension of a plain Signature now returns a full-width Int instead of an
Int8: on 1.10, binomial with a narrow integer falls back to the Float64
gamma-function method, which made nblades of a KVector in a
custom-signature algebra return a Float64 and broke construction.

The zero-allocation test gates assert what the current stable compiler
constant-folds; the 1.10 compiler misses some of those folds, so
ALLOCATION_GATES restricts them to 1.12 and later. Correctness and
inference tests are unchanged and run on every version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant