space::irbem::gpu
cheatah-space v0.1.0-alpha — Biome Standard 0.6.5-alpha
Classes
GpuUnavailable— A device launch on a build or a machine that has no device.KernelInfo— One entry point ofirbem.slang, as the host side knows it.ShaderMissing— A registered kernel whose compiled SPIR-V is not where shader_dir says it should be.UnknownKernel— A kernel name that no entry point inirbem.slangimplements.
Functions
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.
name | the Slang entry-point name to look up. |
a reference to the entry, which has static storage duration.
UnknownKernel | when no entry point of that name is registered. |
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.
none on success; the failure path builds one message string.
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.
true when dispatch_batch would reach the device.
O(1).
none.
Why available is false.
the reason, empty exactly when the device is available.
O(1).
one string (the returned value).
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.
the directory; not checked for existence, which is dispatch_batch's job.
O(1).
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.
Where kernel's compiled SPIR-V is expected.
kernel | the entry-point name; the file is |
the full path.
O(1).
shader_dir's, plus one string and one path for the file name.
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.
the path to irbem.slang.
O(1).
two short-lived paths, one of them returned.
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.
kernel | the registered kernel the batch would run. |
the crossover, possibly never_faster_on_device.
UnknownKernel | when |
O(number of registered kernels).
none.
Whether a batch of points of kernel should take the device lane under Lane::Auto.
kernel | the registered kernel the batch would run. |
points | how many points the batch holds. |
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.
UnknownKernel | when |
O(number of registered kernels).
none.
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.
kernel | the registered entry-point name to launch. |
pos | the input points, xyz-interleaved, |
out | the output vectors, xyz-interleaved, exactly as long as |
params | the kernel's scalar parameters, exactly |
UnknownKernel | when |
std::invalid_argument | when |
ShaderMissing | when the kernel's |
GpuUnavailable | when available is false. |
O(N) device work over ceil(N/256) workgroups, plus O(N) bytes moved each way.
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.
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.
pos | 3N floats, GEO, Earth radii. |
pitch | N floats, degrees. |
coef | IGRF |
norm | the Legendre table. |
ext | the 39-float external block described above. |
dims |
|
out | receives 4N floats: |
status | one word per line. |
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.
One dispatch; O(N x steps) TOTAL-field evaluations (~900 flops each), concurrent.
eight device buffers, returned to the context's size-classed pool on scope exit.
IrbemGpu.TotalFieldTraceAgreesWithTheHostLanebool 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.
pos | 3N floats, GEO, Earth radii. |
pitch | N floats, degrees. |
coef |
|
norm | the Legendre normalisation, |
dims |
|
out | receives 4N floats: |
status | receives N status words, one per line, so a single non-closing line reports itself without spoiling the batch. |
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.
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.
seven device buffers, returned to the context's size-classed pool on scope exit.
IrbemGpu.TraceKernelAgreesWithTheHostLanebool 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.
pos | 3N floats, GEO, Earth radii. |
coef |
|
norm | the Legendre normalisation — |
dims |
|
out | receives 3N floats: the field at each point, GEO, nT. |
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.
std::invalid_argument | when |
One dispatch over ceil(N/256) workgroups.
five device buffers, returned to the context's size-classed pool on scope exit.
IrbemDriftShell.FluxCellsAgreeBetweenLanesbool 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.
pos | 3N floats, GEO, Earth radii — one magnetic-equator seed per shell azimuth. |
dir | N floats, |
coef |
|
norm | the normalisation. |
dims |
|
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. |
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.
std::invalid_argument | when the spans do not agree. |
One dispatch over ceil(N/256) workgroups; O(N × steps) field evaluations, run concurrently.
seven device buffers, returned to the context's size-classed pool on scope exit.
IrbemDriftShell.FootpointsAgreeBetweenLanesThe 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.
p | the point, in the MAG (centred-dipole) frame, in Earth radii. |
g10_nT | the degree-1 order-0 Gauss coefficient, in nanotesla. |
the field at p, in the same frame, in nanotesla; exactly zero at the origin.
O(1) — one square root, one division.
none.
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.
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.
pos | the points, xyz-interleaved, |
out | the field, xyz-interleaved, |
g10_nT | the degree-1 order-0 Gauss coefficient, in nanotesla. |
std::invalid_argument | when |
O(N) — one square root and one division per point, no branch per component.
none. Not one byte: the loop is over caller-provided spans.
The centred-dipole field over a whole batch, on the device.
pos | the points, xyz-interleaved, |
out | the field, xyz-interleaved, |
g10_nT | the degree-1 order-0 Gauss coefficient, in nanotesla. |
GpuUnavailable | when there is no device; ShaderMissing when the kernel was not compiled; |
O(N) device work, plus 2·3N floats over the bus.
the parameter block is a stack std::array; everything else is dispatch_batch's.
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.
pos | the points, xyz-interleaved, |
out | the field, xyz-interleaved, |
g10_nT | the degree-1 order-0 Gauss coefficient, in nanotesla. |
lane | which lane to run; Lane::Auto by default. |
GpuUnavailable | when |
std::invalid_argument | when the spans do not match. |
O(N).
none on the host lane; see dispatch_batch for the device lane.
Constants & variables
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.
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.
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.
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.
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
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.
