cheatah
Module

space::irbem::gpu

cheatah-space v0.1.0-alpha — Biome Standard 0.6.5-alpha

Classes

Functions

fn const KernelInfo & kernel_info(std::string_view name) #

The registry and the lease capacity must agree, checked at COMPILE time: a kernel that binds more buffers than a launch can hold is a build error, not a runtime overrun.

The registry entry for name.

Parameters
name

the Slang entry-point name to look up.

Returns

a reference to the entry, which has static storage duration.

Parameters
UnknownKernel

when no entry point of that name is registered.

Complexity

O(number of registered kernels) — a linear scan over a table of a handful of rows, which beats any hashing at this size and, unlike hashing, is constexpr-friendly.

Allocation

none on success; the failure path builds one message string.

fn bool available() noexcept #

Whether a device launch can run right now.

The runtime half of the availability question: true only when the device stack was compiled in, the operator has not switched it off, and cheatah-gpu-linalg's own cached bring-up probe succeeds. Never throws — that is the point of asking instead of trying.

Deliberately NOT cached here, although the probe underneath it is: the environment override has to take effect when it is set, and the cost of the query is a getenv plus a load of a function-local static.

Returns

true when dispatch_batch would reach the device.

Complexity

O(1).

Allocation

none.

fn std::string unavailable_reason() #

Why available is false.

Returns

the reason, empty exactly when the device is available.

Complexity

O(1).

Allocation

one string (the returned value).

fn std::filesystem::path shader_dir() #

The directory holding this module's compiled .spv files.

Resolution order deliberately mirrors cheatah-gpu-linalg's own spv_bytes, so one habit covers both: the CHEATAH_SPACE_IRBEM_SPV_DIR environment variable, then the same-named compile definition, then <repo>/build/shaders derived from __FILE__ — which means a consumer that passes no definitions at all still finds the kernels of the checkout it compiled against.

Returns

the directory; not checked for existence, which is dispatch_batch's job.

Complexity

O(1).

Allocation

a few short-lived std::filesystem::path temporaries on the __FILE__ fallback, one on the other two; heap, but once per launch and never per point.

fn std::filesystem::path shader_path(std::string_view kernel) #

Where kernel's compiled SPIR-V is expected.

Parameters
kernel

the entry-point name; the file is <kernel>.spv, matching what cheatah-gpu-linalg's context will independently resolve during the launch.

Returns

the full path.

Complexity

O(1).

Allocation

shader_dir's, plus one string and one path for the file name.

fn std::filesystem::path shader_source_path() #

Where the Slang source of this module's kernels lives.

Derived from __FILE__ rather than assumed relative to the working directory, so the registry-completeness test can read the source no matter where it is run from.

Returns

the path to irbem.slang.

Complexity

O(1).

Allocation

two short-lived paths, one of them returned.

fn std::size_t gpu_crossover(std::string_view kernel) #

The batch size at or above which kernel's device lane is worth its transfers.

Normally KernelInfo::crossover_points, i.e. the measurement recorded next to the kernel. CHEATAH_SPACE_IRBEM_GPU_CROSSOVER overrides it process-wide with a decimal batch size, which is what makes the policy re-tunable on a machine whose bus and cores are not this one's without a rebuild — and what lets the suite drive the device lane through Lane::Auto. A value that is not a whole decimal number is ignored rather than read as zero, because reading a typo as "always use the GPU" is the expensive direction to be wrong in.

Parameters
kernel

the registered kernel the batch would run.

Returns

the crossover, possibly never_faster_on_device.

Parameters
UnknownKernel

when kernel is not registered.

Complexity

O(number of registered kernels).

Allocation

none.

fn bool prefer_gpu(std::string_view kernel, std::size_t points) #

Whether a batch of points of kernel should take the device lane under Lane::Auto.

Parameters
kernel

the registered kernel the batch would run.

points

how many points the batch holds.

Returns

true when the kernel has a measured crossover, the batch reaches it, and a device is available; false — meaning "run it on the host" — in every other case.

Parameters
UnknownKernel

when kernel is not registered.

Complexity

O(number of registered kernels).

Allocation

none.

fn void dispatch_batch(std::string_view kernel, std::span< const float > pos, std::span< float > out, std::span< const float > params) #

Run a registered kernel over one batch of xyz-interleaved points, blocking until it completes.

The single door to the device. The order of the checks is itself part of the contract, and each step is diagnosed before the next is attempted: an unregistered kernel (UnknownKernel), then a malformed batch (std::invalid_argument), then a shader that was never compiled (ShaderMissing), then a machine or build with no device (GpuUnavailable). Shader existence is checked BEFORE availability on purpose — a machine with no GPU is exactly where a skipped shader-build step is likeliest, and reporting "no GPU" there would hide it.

Parameters
kernel

the registered entry-point name to launch.

pos

the input points, xyz-interleaved, 3N floats; the batch size N is derived from it.

out

the output vectors, xyz-interleaved, exactly as long as pos; overwritten in full.

params

the kernel's scalar parameters, exactly KernelInfo::params of them.

Parameters
UnknownKernel

when kernel is not in registered_kernels.

std::invalid_argument

when pos is not a whole number of points, when out is not the same length, or when params is the wrong length for the kernel.

ShaderMissing

when the kernel's .spv is not under shader_dir.

GpuUnavailable

when available is false.

Complexity

O(N) device work over ceil(N/256) workgroups, plus O(N) bytes moved each way.

Allocation

no per-point allocation. Per call: a handful of strings and paths for the resolution and the diagnostics, one setenv, and four POOLED device buffers, all released before return. An empty batch allocates nothing and touches no device.

fn bool launch_trace_total(std::span< const float > pos, std::span< const float > pitch, std::span< const float > coef, std::span< const float > norm, std::span< const float > ext, std::span< const std::uint32_t > dims, std::span< float > out, std::span< std::uint32_t > status) #

Launch the TOTAL-field tracer — IGRF plus T89 in one resident trace.

Identical contract to launch_trace, with one more upload: ext carries the T89 parameter block for the batch's Kp bin followed by the epoch's gsm_from_geo rotation, column-major — 39 floats that make the external field evaluable on the device without a host round-trip per RK4 stage. The bin is selected on the HOST because a batch shares one epoch and one Kp; a per-thread bin branch would diverge the warp for nothing.

Parameters
pos

3N floats, GEO, Earth radii.

pitch

N floats, degrees.

coef

IGRF g then h, interpolated to the epoch.

norm

the Legendre table.

ext

the 39-float external block described above.

dims

{N, nmax, max_steps, steps_per_l x 1000}.

out

receives 4N floats: I, Bmin, Bmirr, Blocal.

status

one word per line.

Returns

false when there is no device or no compiled SPIR-V — the caller's cue for the host lane, not an error. Genuine misuse (mismatched spans) still throws.

Complexity

One dispatch; O(N x steps) TOTAL-field evaluations (~900 flops each), concurrent.

Allocation

eight device buffers, returned to the context's size-classed pool on scope exit.

Unit testIrbemGpu.TotalFieldTraceAgreesWithTheHostLane
fn bool launch_trace(std::span< const float > pos, std::span< const float > pitch, std::span< const float > coef, std::span< const float > norm, std::span< const std::uint32_t > dims, std::span< float > out, std::span< std::uint32_t > status) #

Launch the field-line tracer — the seven-binding shape dispatch_batch cannot express.

dispatch_batch above is for the pos/out/params/dims kernels: one vector in, one vector out. The tracer takes positions AND pitch angles AND two coefficient tables, and writes four floats plus a status word per line. Rather than generalise the four-binding helper into something that describes every shape badly, this is the tracer's own launcher — the kernel that carries the module's whole performance argument earns fifty lines of its own.

Parameters
pos

3N floats, GEO, Earth radii.

pitch

N floats, degrees.

coef

g then h, already interpolated to the epoch by the caller.

norm

the Legendre normalisation, constexpr on the host and therefore free.

dims

{N, nmax, max_steps, steps_per_l × 1000}.

out

receives 4N floats: I, Bmin, Bmirr, Blocal per line.

status

receives N status words, one per line, so a single non-closing line reports itself without spoiling the batch.

Returns

false when there is no device or no compiled SPIR-V — the caller's cue to run the host lane, not an error. Genuine misuse (mismatched spans) still throws.

Complexity

One dispatch over ceil(N/256) workgroups; O(N × steps) field evaluations, run concurrently. ~28 bytes in and ~20 out per line for ~10^5 flops — the arithmetic intensity that makes this worth offloading at all.

Allocation

seven device buffers, returned to the context's size-classed pool on scope exit.

Unit testIrbemGpu.TraceKernelAgreesWithTheHostLane
fn bool launch_igrf(std::span< const float > pos, std::span< const float > coef, std::span< const float > norm, std::span< const std::uint32_t > dims, std::span< float > out) #

Launch the IGRF field kernel over a batch of arbitrary points — the five-binding shape.

dispatch_batch cannot express this one: it uploads a scalar parameter block, and IGRF has no scalars, only two tables. The tables arrive ALREADY interpolated to the epoch, because interpolating IGRF's 26-epoch table per thread would be one redundant copy per point of a calculation the host does once per batch.

L*'s flux integral is what made this worth exposing on the seam rather than leaving it inside a benchmark: Phi is one field evaluation per polar-cap cell, ~2 400 cells per L* point and nothing else — exactly the ~20 flops/byte regime the registry measures the device winning by 8.37x in.

Parameters
pos

3N floats, GEO, Earth radii.

coef

g then h, pre-interpolated.

norm

the Legendre normalisation — constexpr on the host, so free to produce.

dims

{N, nmax}.

out

receives 3N floats: the field at each point, GEO, nT.

Returns

false when there is no device or no compiled SPIR-V — the caller's cue to run the host lane, not an error. Genuine misuse (mismatched spans) still throws.

Parameters
std::invalid_argument

when out is not the same length as pos.

Complexity

One dispatch over ceil(N/256) workgroups.

Allocation

five device buffers, returned to the context's size-classed pool on scope exit.

Unit testIrbemDriftShell.FluxCellsAgreeBetweenLanes
fn bool launch_shell_foot(std::span< const float > pos, std::span< const float > dir, std::span< const float > coef, std::span< const float > norm, std::span< const std::uint32_t > dims, std::span< float > out, std::span< std::uint32_t > status) #

Launch the drift-shell footpoint tracer — the second seven-binding shape.

The companion to launch_trace, and the step that turns a converged drift shell into the polar-cap boundary L*'s flux integral is taken over. Each line walks from its magnetic-equator seed down to r = 1 along dir, halving its step onto the sphere rather than interpolating across it; see the kernel's own header in irbem.slang for why that distinction is worth ~30 extra steps.

Parameters
pos

3N floats, GEO, Earth radii — one magnetic-equator seed per shell azimuth.

dir

N floats, +1 or -1: which way along B leads to the chosen hemisphere. The host decides, because the kernel works in GEO and has no dipole axis to compare against.

coef

g then h, already interpolated to the epoch.

norm

the normalisation.

dims

{N, nmax, max_steps, steps_per_l × 1000}.

out

receives 3N floats: each footpoint in GEO, ON the unit sphere.

status

receives N status words, so one line that never reaches the surface reports itself instead of spoiling the batch.

Returns

false when there is no device or no compiled SPIR-V — the caller's cue to run the host lane, not an error. Genuine misuse (mismatched spans) still throws.

Parameters
std::invalid_argument

when the spans do not agree.

Complexity

One dispatch over ceil(N/256) workgroups; O(N × steps) field evaluations, run concurrently.

Allocation

seven device buffers, returned to the context's size-classed pool on scope exit.

Unit testIrbemDriftShell.FootpointsAgreeBetweenLanes
fn FieldVector< Frame::MAG > dipole_field_at(Position< Frame::MAG > p, double g10_nT) #

The centred-dipole magnetic field at one point, in double — the reference the device lane is measured against.

The degree-1 order-0 term of the IAGA spherical-harmonic expansion that defines IGRF (Alken et al., International Geomagnetic Reference Field: the thirteenth generation, Earth Planets Space 73:49 (2021), eq. 1). With V = a (a/r)^2 g10 P_1^0(cos θ) and B = -∇V, B_r = 2 g10 (a/r)^3 cos θ and B_θ = g10 (a/r)^3 sin θ, which in the MAG frame — where the dipole axis IS — is the single identity B = g10 (a/r)^3 (3 (ẑ·r̂) r̂ − ẑ); componentwise, with a = 1 Re, Bx = 3 g10 x z / r⁵, By = 3 g10 y z / r⁵, Bz = g10 (3z² − r²) / r⁵. The derivation is written out in full in irbem.slang, whose kernel evaluates exactly these expressions in exactly this order.

The real g10 is negative (−29404.8 nT at IGRF-13 epoch 2020.0), which is what makes the field point into the ground at the northern dipole pole; the sign lives in the coefficient, so this routine never encodes a hemisphere.

Parameters
p

the point, in the MAG (centred-dipole) frame, in Earth radii.

g10_nT

the degree-1 order-0 Gauss coefficient, in nanotesla.

Returns

the field at p, in the same frame, in nanotesla; exactly zero at the origin.

Complexity

O(1) — one square root, one division.

Allocation

none.

Note

The origin is a genuine pole of a dipole field, not a rounding problem. Returning zero there is a DEFINED value chosen so the host and the kernel agree on a testable answer instead of disagreeing about which flavour of NaN they produce.

fn void dipole_field_host(std::span< const float > pos, std::span< float > out, float g10_nT) #

The centred-dipole field over a whole batch, on the CPU, in float.

The host twin of irbem_dipole_f32: the same expressions, in the same order, in the same precision, which is what makes a disagreement between the two attributable to the DEVICE (contraction, a driver's transcendental) rather than to the arithmetic having been written differently on the two sides. It is also the lane a machine without a GPU actually runs.

Parameters
pos

the points, xyz-interleaved, 3N floats, in the MAG frame, in Earth radii.

out

the field, xyz-interleaved, 3N floats, in nanotesla; overwritten in full.

g10_nT

the degree-1 order-0 Gauss coefficient, in nanotesla.

Parameters
std::invalid_argument

when pos is not a whole number of points or out is a different length.

Complexity

O(N) — one square root and one division per point, no branch per component.

Allocation

none. Not one byte: the loop is over caller-provided spans.

fn void dipole_field_gpu(std::span< const float > pos, std::span< float > out, float g10_nT) #

The centred-dipole field over a whole batch, on the device.

Parameters
pos

the points, xyz-interleaved, 3N floats, in the MAG frame, in Earth radii.

out

the field, xyz-interleaved, 3N floats, in nanotesla; overwritten in full.

g10_nT

the degree-1 order-0 Gauss coefficient, in nanotesla.

Parameters
GpuUnavailable

when there is no device; ShaderMissing when the kernel was not compiled; std::invalid_argument when the spans do not match.

Complexity

O(N) device work, plus 2·3N floats over the bus.

Allocation

the parameter block is a stack std::array; everything else is dispatch_batch's.

fn void dipole_field(std::span< const float > pos, std::span< float > out, float g10_nT, Lane lane=Lane::Auto) #

The centred-dipole field over a whole batch, on whichever lane lane selects.

The production entry point. Under Lane::Auto the choice is prefer_gpu's, which means a build with no device stack, a machine with no device, and a batch too small to pay for its transfers all quietly run the host lane — while a forced Lane::Gpu on any of those throws, because a test that meant to exercise the device must not silently pass on the CPU.

Parameters
pos

the points, xyz-interleaved, 3N floats, in the MAG frame, in Earth radii.

out

the field, xyz-interleaved, 3N floats, in nanotesla; overwritten in full.

g10_nT

the degree-1 order-0 Gauss coefficient, in nanotesla.

lane

which lane to run; Lane::Auto by default.

Parameters
GpuUnavailable

when lane is Lane::Gpu and there is no device.

std::invalid_argument

when the spans do not match.

Complexity

O(N).

Allocation

none on the host lane; see dispatch_batch for the device lane.

Constants & variables

var bool compiled_with_gpu #

Whether this translation unit was compiled against cheatah-gpu-linalg.

The compile-time half of the availability question, and a constant — usable in if constexpr and in a static_assert for a build that means to require the device stack. It says nothing about whether a device exists on the machine running the binary; that is available.

var std::size_t never_faster_on_device #

The KernelInfo::crossover_points of a kernel the device never wins on.

Not a "not measured yet" placeholder — it is the answer for a transfer-bound kernel, and a kernel carrying it runs on the host under Lane::Auto no matter how large the batch is.

var std::size_t always_on_device #

The KernelInfo::crossover_points of a kernel that should ALWAYS take the device lane.

This is the value the integral kernels carry, and it is what "the GPU is the default" means in practice. It is not a policy override — it is what the arithmetic says. One L* point is roughly 10^5 field evaluations, ~5 x 10^7 flops; at even 10% of this device's fp32 peak that is ~25 us of compute against a measured ~30 us synchronous dispatch floor, so a batch of TWO points already pays for the round trip and every realistic batch dwarfs it. The transfer is ~60 bytes per point for ~5 x 10^7 flops — an arithmetic intensity of ~8 x 10^5 flops/byte, some 350x beyond the PCIe ridge point, which is why the bus simply does not appear in the accounting.

The contrast with never_faster_on_device is the whole reason this is per-kernel rather than one library-wide switch. Measured on this machine, same seam, same transfers, same device:

| kernel | flops/byte | device | host | verdict | | dipole | 0.5 | 243 Mpts/s | 352 Mpts/s | 0.69x LOSES | | IGRF-14 | ~20 | 27.3 Mpts/s | 3.3 Mpts/s | 8.37x WINS | | trace | ~9 400 | the integrals — the point never leaves the device |

A blanket "always use the GPU" would make the dipole kernel 45% SLOWER. Defaulting to the device where the arithmetic earns it, and only there, is the honest form of the same intent.

Note

On a UNIFIED-MEMORY device — an APU, an integrated GPU, Apple silicon — there is no PCIe copy at all, so the transfer term that makes the dipole lose largely vanishes and even transfer-bound kernels may win. cheatah-gpu-linalg reads VkPhysicalDeviceProperties:: deviceType when it scores devices but does not expose it, so this header cannot yet ask. Until it can, the crossovers here are the DISCRETE-GPU measurement, which is the conservative direction: a unified-memory device will beat them, never miss them.

var std::array< KernelInfo, 11 > registered_kernels #

Every kernel irbem.slang implements.

A constexpr table rather than a function, so it costs nothing, can be iterated at compile time, and cannot drift into a registry that allocates. Adding a kernel means adding a row here AND an entry point there; the completeness test fails if only one of the two happens.

Types

enum std::uint8_t Lane #

Which arithmetic lane a batch routine runs on.

Auto is what production uses; the two explicit values exist because the differential suite has to be able to run the SAME inputs through both lanes and compare, which it cannot do if the choice is only ever made for it.