Ayoob AI

Preventing Silent Numerical Degradation in GPU-Accelerated Finance AI

·17 min read·Ayoob AI
WebGPUFloat32PrecisionFinanceHealthcare

The constraint that makes GPU finance dangerous

WGSL, the shading language for WebGPU, supports f32 as its floating-point type. The f16 extension exists for half-precision. There is no f64. The WebGPU specification does not include a double-precision extension, and no browser vendor has signalled intent to add one.

Every number that enters a WebGPU compute shader is 32-bit. Every arithmetic operation produces a 32-bit result. Every number that comes back is 32-bit.

JavaScript uses IEEE 754 double-precision (Float64) for all numeric values. When you call device.queue.writeBuffer() with a Float32Array view of your data, the runtime narrows every 64-bit value to 32 bits. When you read results back via mapAsync(), you receive 32-bit values that your JavaScript code silently promotes back to 64-bit.

The promotion does not restore the lost precision. A Float64 value of 33554432.75 narrows to Float32 as 33554432.0. When the Float32 result is promoted back to Float64, it becomes 33554432.0 in 64-bit representation. The .75 is permanently gone. It was lost at the type boundary on the way in. No operation on the way out can recover it.

This is not a rounding error in the traditional sense. Rounding errors accumulate gradually through arithmetic. This is a truncation that happens before any computation begins, at the moment of data transfer.

What Float32 cannot represent

Float32 has a 23-bit significand (plus one implicit leading bit, giving 24 bits of effective precision). This means it can represent integers exactly up to 2^24: 16,777,216. Above that boundary, the spacing between consecutive representable Float32 values exceeds 1:

MagnitudeGap between consecutive Float32 valuesImplication
Up to £16,777,216≤ 1Penny-level accuracy possible
£16,777,217 to £33,554,4322Nearest representable value is the nearest even pound
£33,554,433 to £67,108,8644Nearest £4
£67,108,865 to £134,217,7288Nearest £8
£134,217,729 to £268,435,45616Nearest £16
Above £268,435,45632+Nearest £32 or worse

A single-property commercial mortgage of £45,000,000 cannot be represented to the nearest pound in Float32. The nearest representable values are £44,999,996 and £45,000,000. You might get lucky on the exact value. You will not get lucky on the interest calculation, the amortization schedule, or the present value computation derived from it.

Float64, by contrast, has a 52-bit significand (53 effective bits). Exact integer representation up to 2^53: 9,007,199,254,740,992. For financial values stored as pence or cents, Float64 is exact for any amount below £90 trillion. This is why JavaScript's number type works for financial computation despite lacking a native decimal type. And it is why losing 29 bits of significand precision on the GPU is not a minor degradation.

Silent numerical degradation

The dangerous property of Float32 narrowing is that it produces valid numbers. Not NaN. Not Infinity. Not an error. Valid, plausible, wrong numbers.

const portfolioValue = 45000000.75;  // Float64: exact
const f32 = Math.fround(portfolioValue);  // Float32: 45000000.0
console.log(f32);  // 45000000
// No error. No warning. The 75p is gone.

Math.fround() performs the exact narrowing that device.queue.writeBuffer() performs when writing to a Float32 GPU buffer. The result passes every validation check: it is a positive number, it is in the expected range, it is within 0.000002% of the true value. Every schema validator, every type checker, every assertion that does not know about Float32 precision limits will accept it.

This is why we call it silent numerical degradation. The system does not fail. It does not warn. It produces a number that looks correct and is not.

Three domains where silence is catastrophic

Regulatory finance. MiFID II transaction reporting requires exact values. A reported trade value of £45,000,000 when the true value is £45,000,000.75 is a reporting error. At scale (millions of transactions per quarter), these errors aggregate. They appear in reconciliation breaks that compliance teams cannot explain, because the code is correct and the logic is correct. Only the precision is wrong.

Healthcare dosimetry. Radiation treatment planning computes dose distributions across tissue volumes. The calculations involve matrix operations on attenuation coefficients. A Float32 rounding error in the 6th significant digit on a coefficient can shift the computed dose boundary by millimetres. In radiation oncology, millimetres determine whether the beam hits the tumour or the healthy tissue behind it.

Portfolio optimization. Mean-variance optimization solves a quadratic program over a covariance matrix. The covariance matrix of financial returns is often ill-conditioned (condition numbers of 5,000 to 50,000 are common). Float32 rounding errors in the matrix entries are amplified by the condition number through the solve step. The resulting portfolio weights can diverge meaningfully from the Float64 solution.

How error amplifies through computation

Not all operations amplify Float32 rounding errors equally. The amplification depends on the mathematical structure of the operation.

Low amplification: element-wise operations

A filter (WHERE value > threshold) compares two numbers. The comparison result is correct as long as the Float32 representations of both numbers are on the same side of the threshold. For values far from the threshold, Float32 and Float64 produce identical boolean results. For values within 1 ULP of the threshold, the result may differ. The error rate is proportional to the fraction of values within the Float32 ULP of the threshold boundary. For most practical filters, this fraction is negligible.

A sort orders elements by relative magnitude. Float32 preserves the total order of values (our IEEE 754 bit-transform relies on this property). Two Float64 values that map to distinct Float32 representations will sort identically in both precisions. Only values that collide in Float32 (two Float64 values that round to the same Float32) may swap order. For most financial datasets, collisions are rare.

These operations are safe for GPU dispatch in most cases.

Medium amplification: accumulation

Summing N values accumulates rounding error. In the worst case (adversarial ordering), the accumulated error from Float32 addition is proportional to N * ε * max(partial_sums), where ε is the Float32 machine epsilon (1.19 x 10^-7).

For 500,000 revenue values averaging £400, the true sum is £200,000,000. The Float32 sum can differ by up to:

500,000 * 1.19e-7 * 200,000,000 ≈ £11,900

In practice, the error is usually smaller (random rounding errors partially cancel through statistical cancellation). But "usually smaller" is not a guarantee. Compensated summation algorithms (Kahan summation) reduce the error bound, but WGSL does not provide them natively, and implementing them doubles the instruction count per addition.

More fundamentally, the sum itself (£200,000,000) exceeds Float32's exact integer range (16,777,216). The final result cannot be represented exactly in Float32 regardless of the summation algorithm. The best Float32 can do is round to the nearest representable value, which at this magnitude has a gap of 16. The result is correct to the nearest £16 at best.

High amplification: linear algebra

Solving a linear system Ax = b amplifies input perturbations by the condition number κ(A) of the matrix. The relative error in the solution is bounded by:

||δx|| / ||x|| ≤ κ(A) * ||δb|| / ||b||

Where δb is the perturbation introduced by Float32 rounding. The input perturbation from Float32 is approximately ε = 1.19 x 10^-7 (one ULP in the worst case). The output perturbation is κ(A) * 1.19 x 10^-7.

For a well-conditioned system (κ = 10): output error is 1.19 x 10^-6. Manageable.

For a moderately conditioned covariance matrix (κ = 12,000): output error is 1.43 x 10^-3. On a £100,000,000 portfolio, that is £142,800 of error in the optimal allocation.

For an ill-conditioned system (κ = 100,000, not uncommon in multi-factor risk models): output error is 1.19 x 10^-2. On £100,000,000, that is £1,190,000. The "optimal" portfolio weights are meaningless. The solver has returned noise.

Our Precision Sufficiency Analyser

The analyser runs before any GPU dispatch. It evaluates the operation's sensitivity to Float32 precision loss and produces a precision risk score. If the score exceeds the caller's tolerance, the Float32 Safety Guard blocks GPU dispatch.

HIGH-sensitivity path: condition number estimation

For linear algebra operations (matrix solve, least-squares, eigenvalue decomposition, Cholesky factorization), the analyser estimates the condition number κ(A) of the input matrix. The patent specifies a base threshold of infinity for Solve operations, meaning they are always routed to CPU.

Computing the exact condition number requires a singular value decomposition (SVD), which is O(n^3) for an n x n matrix. That is as expensive as the solve itself. We use Hager's condition number estimator, which computes a lower bound on the 1-norm condition number in O(n^2) time.

Hager's algorithm works by iterating the following sequence:

  1. Solve Ax = b for a probe vector b.
  2. Compute the sign vector s = sign(x).
  3. Solve A^T z = s.
  4. If ||z||_inf > z^T x, update the estimate and repeat. Otherwise, the current estimate is a lower bound on ||A^-1||_1.

The condition number estimate is κ_1(A) = ||A||_1 * ||A^-1||_1. The algorithm typically converges in 2 to 4 iterations. For a 200 x 200 matrix, total cost is under 5 ms. The solve itself takes 40 to 80 ms on CPU. The estimation overhead is under 10% of the operation cost.

Once κ is estimated, the precision risk score is:

expectedRelativeError = κ * FLOAT32_EPSILON    // κ * 1.1920929e-7
precisionRiskScore = expectedRelativeError / userTolerance

The user tolerance defaults to 1 x 10^-9 for financial workloads. This is configurable per operation.

Example: A 150 x 150 covariance matrix from 2 years of daily returns on 150 assets. Estimated κ = 12,000. Expected relative error: 12,000 * 1.19 x 10^-7 = 1.43 x 10^-3. User tolerance: 1 x 10^-9. Precision risk score: 1.43 x 10^6. The score exceeds 1.0 by six orders of magnitude. The Safety Guard blocks GPU dispatch.

The same operation evaluated against Float64 machine epsilon (2.22 x 10^-16): expected relative error = 12,000 * 2.22 x 10^-16 = 2.66 x 10^-12. Risk score against 1 x 10^-9 tolerance: 0.00266. Well within bounds. CPU Float64 dispatch proceeds.

MEDIUM-sensitivity path: accumulation overflow analysis

The patent classifies GEMM, GEMV, and Conv2D as MEDIUM sensitivity because their inner-loop dot products involve large accumulations that can overflow Float32 precision. The analyser checks accumulation overflow bounds for these operations.

Reduction operations (summation, averaging, running totals) are classified as LOW sensitivity in the patent, but the range check applied to them similarly catches cases where the accumulated value exceeds Float32's safe integer threshold. The following analysis applies to any operation where accumulation bounds must be verified.

maxAccumulation = estimateMaxAccumulation(dataset, operation)
precisionRiskScore = maxAccumulation / FLOAT32_SAFE_INTEGER  // divided by 16,777,216

The estimation scans the input column's statistics (sum of absolute values from the histogram, or max * count as a conservative upper bound) without iterating the full dataset.

Example: SUM(revenue) over 500,000 records. Column statistics: min = £12, max = £850,000, mean ≈ £400 (from histogram). Conservative max accumulation: 500,000 * £400 = £200,000,000. Risk score: 200,000,000 / 16,777,216 = 11.9. Score exceeds 1.0. GPU dispatch blocked.

Example: SUM(quantity) over 500,000 records. Column statistics: min = 0, max = 50, mean ≈ 3.2. Conservative max accumulation: 500,000 * 3.2 = 1,600,000. Risk score: 1,600,000 / 16,777,216 = 0.095. Score is below 1.0. GPU dispatch permitted.

The same dataset, two different columns, two different dispatch decisions. Revenue sums on CPU. Quantity sums on GPU. Both correct.

LOW-sensitivity path: range check and comparison gap analysis

The patent classifies elementwise, unary, reduce, and FFT operations as LOW sensitivity, requiring only a range check. For filters, sorts, and classifications that produce boolean or ordinal results (not numeric outputs), the analyser estimates whether Float32 rounding can alter any comparison result.

minGap = estimateMinimumGap(dataset)
ulpAtMagnitude = computeULP(estimateMaxMagnitude(dataset))
precisionRiskScore = ulpAtMagnitude / minGap

If the smallest gap between adjacent values in the dataset exceeds the Float32 ULP at the relevant magnitude, every comparison will produce the same result in Float32 as in Float64. The GPU result is bit-identical to the CPU result for the purpose of filtering and ordering.

For financial data with penny precision (minimum gap = 0.01) and magnitudes under £1,000,000 (ULP at 10^6 ≈ 0.0625), the risk score is 0.0625 / 0.01 = 6.25. Score exceeds 1.0. Some comparison results may differ.

For the same data with a filter threshold far from any cluster of values (e.g., WHERE revenue > 100000 when very few values are near 100,000), the practical risk is near zero even though the theoretical risk score is above 1.0. The analyser uses the column histogram to estimate how many values fall within 1 ULP of the threshold. If the count is zero, the effective risk score is overridden to 0.

The Float32 Safety Guard

The Safety Guard is the enforcement layer. When the Precision Sufficiency Analyser produces a risk score above 1.0, the Safety Guard injects a categorical penalty into the dispatch scoring function.

The penalty is negative infinity. This is the same mechanism used by our Categorical GPU Inhibition for branch divergence and our atomic contention threshold for output density. The dispatch score becomes negative infinity regardless of the other six factors in the 7-factor scoring function (size-vs-threshold, sparsity penalty, GPU class bonus, vendor tuning, fusion bonus, arithmetic intensity).

No dataset size makes Float32 precise enough for a £200 million sum. No GPU speed advantage compensates for returning £200,000,016 instead of £200,000,000.50. The penalty is absolute.

Audit logging

Every Safety Guard intervention produces a structured log entry:

{
  "timestamp": "2026-04-14T09:23:41.182Z",
  "operation": "MATRIX_SOLVE",
  "sensitivityTier": "HIGH",
  "conditionNumber": 12000,
  "expectedRelativeError": 1.43e-3,
  "userTolerance": 1e-9,
  "riskScore": 1430000,
  "action": "GPU_DISPATCH_BLOCKED",
  "fallbackTier": "CPU_FLOAT64",
  "reason": "CONDITION_NUMBER_EXCEEDS_FLOAT32_ERROR_BOUND"
}

For regulated industries, this log provides audit evidence that the system evaluated precision risk and took corrective action. The log does not say "we think this might be inaccurate." It says: "the estimated error (1.43 x 10^-3) exceeds the configured tolerance (1 x 10^-9) by a factor of 1,430,000. GPU dispatch was blocked. The operation executed on CPU with Float64 precision."

An auditor can verify the mathematical basis of the decision. The condition number is recorded. The epsilon is a hardware constant. The tolerance is a policy setting. The decision follows deterministically from these three values.

Tolerance configuration

The default tolerance (1 x 10^-9) is conservative. Some use cases accept lower precision:

DomainTypical toleranceRationale
Regulatory reporting1 x 10^-15 (Float64 exact)Exact penny values required
Portfolio optimization1 x 10^-9Error below £0.001 on £1M positions
Risk aggregation1 x 10^-6Error below £1 on £1M aggregates
Approximate analytics1 x 10^-3Error below £1,000 on £1M totals
Visualization only1 x 10^-2Sub-pixel precision sufficient

A dashboard rendering a bar chart of revenue by region does not need penny accuracy. Setting tolerance to 1 x 10^-2 allows the GPU to handle the aggregation (risk score drops below 1.0 for many accumulation workloads), delivering faster chart updates without compromising visual accuracy.

The tolerance is set per operation, not globally. A single dashboard can use 1 x 10^-15 for the regulatory export button and 1 x 10^-2 for the interactive chart. The Safety Guard evaluates each independently.

Healthcare: where the stakes are clinical

Financial errors cost money. Healthcare errors cost lives. The precision requirements are stricter, the tolerance for silent degradation is zero, and the regulatory framework (FDA 21 CFR Part 11 for software validation, IEC 62304 for medical device software) demands documented evidence that numerical accuracy was verified.

Dosimetry planning

Radiation therapy treatment plans compute dose distributions by solving the Boltzmann transport equation (or simplified models like pencil beam or collapsed cone convolution) across a 3D tissue volume. The computation involves matrix operations on attenuation coefficients, tissue density maps, and beam geometry parameters.

A Float32 rounding error in the 6th significant digit of an attenuation coefficient propagates through the dose calculation. For a typical treatment plan:

  • Tissue density: 1.04 g/cm^3 (soft tissue). Float32 represents this exactly.
  • Attenuation coefficient at 6 MV: 0.04942 cm^-1. Float32 rounds to 0.049420002. The error is in the 8th significant digit. Negligible in isolation.
  • But the dose calculation multiplies this coefficient across a path length of 15 cm, exponentiates the result, and sums contributions from 50+ beam angles. Each step introduces Float32 rounding. The accumulated error can shift the computed 50% isodose line by 0.5 to 2 mm.

In radiation oncology, the planning target volume (PTV) margin is typically 3 to 5 mm. A 2 mm shift in the computed dose boundary consumes 40% to 67% of the safety margin. That is not an abstract precision concern. It is a clinical risk.

Our Safety Guard evaluates the treatment planning computation as a HIGH-sensitivity linear algebra operation. The condition number of the transport matrix determines whether Float32 is safe. For typical treatment geometries, it is not. The computation routes to CPU Float64.

Pharmacokinetic modelling

Drug concentration modelling solves systems of ordinary differential equations (ODEs) describing absorption, distribution, metabolism, and excretion. The parameters (rate constants, volumes of distribution, clearance rates) span 4 to 5 orders of magnitude. ODE solvers with stiff systems are sensitive to the precision of these parameters.

The Precision Sufficiency Analyser evaluates the condition of the Jacobian matrix at each integration step. For stiff systems (common in multi-compartment pharmacokinetic models), the condition number can exceed 10^6. At Float32 precision, the expected relative error exceeds 10%, rendering the simulation results meaningless.

The Safety Guard blocks GPU dispatch. The solver runs on CPU Float64. The clinical team receives accurate concentration-time curves.

Integration with the adaptive compute stack

The Precision Sufficiency Analyser is one of three independent safety systems that can block GPU dispatch:

  1. Branch divergence classifier. Blocks operations with per-element conditional branching that would serialize GPU execution.
  2. Atomic contention profiler. Blocks operations where output density exceeds 10%, causing non-linear throughput collapse.
  3. Precision Sufficiency Analyser. Blocks operations where Float32 arithmetic cannot meet the caller's accuracy tolerance.

Each system evaluates independently. Each can inject a negative infinity penalty. The GPU path runs only when all three confirm safety. This layered architecture means the engine never dispatches a workload that is divergent, contended, or imprecise.

When the Safety Guard forces CPU dispatch, the pipeline fusion engine handles the tier transition cleanly. If the preceding operator was GPU-dispatched, the intermediate result is read back from the GPU. The precision-sensitive operation runs on the CPU. If the subsequent operator is GPU-eligible, the result is uploaded back. The fusion bonus for the subsequent operator resets (no GPU residency from the CPU operation), but the pipeline continues without error.

If the GPU device is lost during a mixed pipeline, the fallback is always to CPU Float64. The CPU path is the safe path in every sense: safe from device loss, safe from precision loss, safe from divergence.

The principle

Speed is not the primary metric. Correctness is. The GPU is a tool for making correct computations faster. It is not a tool for making fast computations correct.

Our engine uses the GPU when it can prove the result will be correct within tolerance. It uses the CPU when it cannot. The proof runs before the computation, not after. The Safety Guard does not check whether the GPU result matches the CPU result. It predicts whether it will, using the mathematical properties of the operation and the precision characteristics of the hardware.

This is the standard for enterprise AI automation infrastructure in regulated industries. You do not deploy a financial calculation that might be wrong and check later. You deploy a system that prevents wrong calculations from executing in the first place. The audit log proves it. The mathematics guarantees it. The Safety Guard enforces it.

Want to discuss how this applies to your business?

Book a Discovery Call