FP Optimization Guide - Choosing a Type and Using It Well#
The CCCL FP component offers several ways to get arithmetic the hardware does not sell directly, and
they are not interchangeable: each one spends a different pipe, holds a different number of bits,
and keeps or drops different parts of double’s semantics. This guide is the cross-component
view. It says how to choose between them, how the types combine inside one kernel, how to get the
most out of each, and how to use the two instruments so that those decisions rest on measurements
rather than guesses. The component pages - fpmp,
fpemu,
fptool custom and
fptool stat - describe each type; this document
assumes them and does not repeat them.
Choosing the type#
Four questions settle most choices, and they are worth answering in this order.
What does the target do to FP64? The rate at which the part runs FP64 against FP32 decides more than anything in the code does. The CUDA Best Practices Guide gives it per compute capability, as results per clock per SM: 1:2 on A100, H100 and B200, and 1:64 on every part that rations FP64 - L40S, B300, RTX PRO 6000 Blackwell and the consumer GPUs - where FP64 issues 2 results per clock per SM against 128 for FP32. Everything below
doublein the table is a way of avoiding the FP64 pipe and pays only where it is rationed;fp64mp2spends it and pays only where it is not.How many significand bits does the computation need?
floathas 24,fp32mp246,double53,fp64emu_unpacked62 between operations,fp64mp2104, binary128 113 - all counted with the implicit leading bit, ascuda::std::numeric_limits::digitscounts them, so the pair figures are 2×24 − 2 and 2×53 − 2 rather than stored-bit totals. When the answer is not known, fp64_custom exists to find it.Which of
double‘s semantics must survive? Its exponent range, whichfp32mp2does not provide; bit-for-bit reproducibility, which onlydoubleitself and packedfp64emuathighoffer; subnormals, infinities and NaNs full IEEE-754 compliance which thelowandmidlevels of both arithmetic components drop.What else does the kernel need? Transcendentals, which
fpmphas andfpemudoes not; atomics; bytes per value, which decide bandwidth and occupancy; and whether the same code has to run on the host.
The table puts the answers together. Speed figures are from the π integration the component pages
measure, relative to native double on the same part, and “rationed” means a part whose FP64 rate
is 1:64.
Need |
FP64 on the target |
Reach for |
Why |
|---|---|---|---|
More than 104 significand bits, or a wider exponent range than |
any |
software binary128 ( |
nothing narrower holds it |
Beyond |
full rate |
|
7.8× faster than software binary128 per |
Beyond |
rationed |
software binary128, or |
binary128 is integer code and runs 1.4–4× faster than |
An x87 |
full rate |
|
104 bits covers 64 with room; the exponent range does not carry over |
Extra precision between operations only, result stored as |
rationed |
|
62-bit intermediates rounded once at the pack; about 1.5× |
|
rationed |
|
identical results off the FP64 pipe; near |
About |
rationed |
|
46 bits in 8 bytes on the FP32 pipe; 5.5–16× |
|
rationed |
|
the cheapest emulated arithmetic, about 2× |
Up to |
full rate |
native |
every emulation only slows the mix down |
To find out how much precision an algorithm needs |
any |
|
sweeps formats within |
To know what the pair arithmetic is doing |
any, on the device |
|
bit-identical results + statistic record |
Three forks in that table deserve more than a row:
Around
doubleon a rationed HW target, three types compete, and the trade is sharp.fp32mp2is the fastest by a wide margin and carries a full math API, but holds 46 bits infloat’s exponent range, so it overflows at 1038 and asks for a cast wherever adoubleenters. Packedfp64emukeeps every bit ofdouble’s semantics and converts as freely asdoubledoes, but at its best it only matches hardwaredouble’s speed and in a large kernel it falls behind - its point is exactness without the FP64 pipe, not throughput.fp64emu_unpackedis the one that beatsdoubleon both speed and accuracy over a chain, at 16 bytes per value, with explicit conversions, without bit-for-bit agreement withdouble.Above
double, the choice betweenfp64mp2andfp64emu_unpackedis a choice about how much extra precision and where it lives. The pair carries 104 bits in the stored value, so a result keeps them; the unpacked form carries 62 between operations and rounds to 53 at the pack, so only the chain benefits. The second is far cheaper and runs on the FP32 and integer pipes, so it is the one to consider on a rationed part when the goal is adoubleresult more accurate thandoublearithmetic would give, rather than a wider result.Below
double,fp64emu_lowisfloat-precision arithmetic withdouble’s range, for computations such as power series whose intermediates leavefloat’s exponent long before the sum converges.
Mixed cases#
The types are designed to be mixed, some real kernels end up using two of them: one for the bulk of the work and one for the part that sets the accuracy. The patterns below are the ones the component documentation already relies on, collected in one place.
A fast level for the kernel, a careful step inside it. The accuracy level is part of an
fpmptype, butadd<>,sub<>,mul<>,div<>,fma<>andmad<>take it per operation and return the operand type, so a kernel written infp32mp2_lowcan run one addition athighwithout changing anything else. Argument reductions and the subtraction of nearly equal values are the usual candidates. The reverse mixing - alowvalue handed tomidorhighcode - is handled by the conversion, which renormalizes on the way; see Accuracy levels.doubledata, a wider accumulator. Inputs that are exact asdoublestay indoublearrays; only the running sum, or the sum of products, outgrows them. Anfp64mp2accumulator fed withacc += dis compensated summation written for you, at 10 FP64 instructions per term, and the terms never leave their 8-byte storage. On a rationed part the same role goes tofp64emu_unpacked, which absorbs the summation error in its guard bits and rounds once at the end.Packed storage, unpacked arithmetic. With
fpemu, the 8-byte packed form belongs in memory and the 16-byte unpacked form in registers: unpack once on load, run the chain, pack once on store.fma,mad,dotandcmulare fused for the same reason, so that intermediate products are not rounded and repacked between steps. Alternating the two representations inside a loop pays the pack/unpack tax the unpacked form exists to avoid, and adoubleliteral in an unpacked expression is such an alternation, since entering the unpacked form is a conversion written out; hoist constants asfp64emu_unpackedvalues.Emulated arithmetic, native transcendentals.
fpemuhas+ - * /,fma,mad,sqrt,dotandcmul, and nothing else. A kernel that needs an occasionalexporsinconverts todouble, calls the native function and converts back - one FP64 call per element, acceptable where the call is rare - moreover this case may get performance benefits from GPU pipes parallelization - see the next case.Two pipes at once. On a rationed part the FP64 units sit idle while
fpemuruns on the FP32 and integer pipes, and would still deliver their own rate if asked, so a reduction split between nativedoubleandfpemucould in principle sum the two throughputs - the fpemu page measuresfp64emu_unpacked_midat 1.78–1.96×double, so the sum would be 2.8–3.0×double. No such kernel has been built or measured, and the two halves share issue bandwidth, registers and the scheduler, so treat it as an upper bound to test rather than a result to plan on. Packedhighis the partner when the split must not change the result: it holdsdouble’s layout and returnsdouble’s bits, so the handover costs nothing and the two halves agree. The unpacked form is the partner when throughput matters more, its pack boundary being where the handover would sit.Size first, then choose. When the precision a computation needs is not known,
fp64_custom<11, M>answers it before any type is committed to: sweepMdownward with the reference reduction kept wide, find where the digits give way, and read the result against the table above - 24 bits point atfloatorfp64emu_low, up to 46 atfp32mp2, 53 atdoubleorfp64emu, more atfp64mp2. A separate sweep of the exponent axis says whetherfloat’s range would have been the problem.
Using fpmp efficiently#
fp32mp2 is written to take double’s place in existing code, and fp64mp2 to take the
place of a wider CPU type - binary128, or the x87 80-bit long double of code being ported to the
GPU - but neither has the cost model of the type it replaces: a pair operation is a run of hardware
operations on the limbs, and how long a run depends on the operation and on the accuracy level. The
table gives the length, in instructions on the floating-point pipe of the component type, from the
SASS counts in the specification - one representative count per operation. The
algorithms are the same for both widths, so the counts are too; what differs is the pipe they
occupy, FP32 for fp32mp2 and FP64 for fp64mp2, and that is the whole of the hardware
question.
Operation |
|
|
|
|---|---|---|---|
|
8 |
11 |
20 |
|
7 |
10 |
13 |
|
5 |
9 |
9 |
|
13 |
16 |
29 |
|
16 |
19 |
37 |
|
6 |
13 |
23, plus 23 integer |
|
11, plus 27 integer |
18, plus 25 integer |
28, plus 49 integer |
|
— |
18 ( |
— |
|
— |
25, integer & fp32 |
— |
|
— |
40, integer & fp32 |
— |
Three things follow from the table before any type-specific advice:
Multiplication costs the same at
midandhigh, sohighis paid for on additions and divisions.Division is the expensive basic operation, and the one
highmakes most expensive.And for
fp32mp2, a conversion to or fromdoublecosts as much as two to four additions - on B300 the two conversions run at 1290 and 734 billion per second against 2782 billionmidadditions.
The statistics type, fp32mp2_stat / fp64mp2_stat from <cuda/fptool>, is the tool several
of the points below lean on. It computes bit-identical results and records, per instrumented
variable, the operation mix, the cancellation and overflow events, and how the limbs were placed;
Using fptool below and
the fpmp2_stat page say how to read the record. It
is a diagnostic rather than a mode: an instrumented run is two to three orders of magnitude slower,
so instrument one variable on a reduced problem and build without it for the runs that count.
fp32mp2#
Use it only where FP64 throughput is much lower than FP32. The type pays for its digits in FP32 instructions, and the bargain holds only where those are cheap relative to FP64. On the FP64-rationed parts - L40S, RTX PRO 6000 Blackwell, B300, consumer GPUs generally - a
midaddition runs 5.6× faster than a nativedoubleaddition, and the well-known π integration benchmark on the fpmp page comes out 8.6–10.8× ahead. On a full-rate part such as B200 the same addition runs at 0.36× of nativedouble:doubleis both faster and more accurate there, and the float pair has nothing to offer. The code is portable; the win is not. Decide from the FP64:FP32 rate of the target, and keepdoublereachable behind a type alias.Keep
double↔fp32mp2conversions out of hot loops. A conversion is arithmetic, not a register move: about 25 instructions into the pair and 40 out of it. They hide in two places. One is adoublevariable in a pair expression -x * scalewithdouble scale- which the mixed-type operator converts on every evaluation before running the full pair operation; a literal can be folded at compile time, a variable cannot. Hoist such scalars intofp32mp2values before the loop. The other is storage. An array offp32mp2is 8 bytes per element, exactly likedouble, so keeping the data in pair form costs no bandwidth and saves a conversion on every load and store; convert once, at the I/O boundary.Stay at
mid, and drop tolowonly on the paths that dominate the profile.midrenormalizes after every operation, so results do not degrade along a chain, at 11 instructions per addition against 8 forlowand 20 forhigh.lowsaves a quarter on additions and nearly half on multiplications by skipping the closingfast_two_sum, and the price is drift: the limbs are free to overlap, and along a long chain part of the trailing limb stops carrying information. The remedy isrenormalize(), three instructions that restore the invariant exactly, placed where the chain ends rather than where each operation does: at the end of the inner loop, every so many iterations of a long accumulation, or where the value is handed to code that assumes normalization - a conversion out oflowintomidorhighdoes this on its own.fp32mp2_stat_lowsays whetherlowis safe on your data, and where:overlap_countandmin_hi_lo_gapshow how often and how far the limbs drift, andinvert_counthas to stay zero. The pattern is thelowtype for the kernel andadd<fpmp2_accuracy::high>for the one step that needs more, as in Accuracy levels.Use
highonly on accuracy-critical steps - reductions above all.highrecovers the rounding error committed when the two trailing limbs are added, and that error only grows into the result when something amplifies it; cancellation is the usual amplifier. It costs 20 instructions per addition against 11, and 46 per division against 13, while multiplication is 9 at both levels - a product-heavy kernel gains nothing fromhighand pays for it on every addition. Where it pays is where nearly equal values are subtracted, where a long sum carries terms of both signs, and in argument reductions.fp32mp2_statpoints at those places:partial_cancel_countandfull_cancel_countcount the operations where significance was lost, per instrumented variable. Then raise the level per operation, withadd<>,sub<>and the rest on the critical step, rather than raising the type for the whole kernel.The conversion macros trade FP64 instructions for registers.
CCCL_FPMP_OPTIMIZED_DOUBLE_TO_FPMPandCCCL_FPMP_OPTIMIZED_FPMP_TO_DOUBLE, both on by default, implement thedouble↔fp32mp2conversions in integer and FP32 arithmetic so that no FP64 instruction sits on the hot path. The cast-based conversions they replace are two and three FP64 operations: shorter, with fewer live registers, but on the rationed FP64 pipe. Setting either to0is worth trying in two cases - a large kernel whose register count or spills, as-Xptxas -vreports them, are limiting occupancy, and a part where FP64 is cheap anyway, where the integer path is extra instructions for nothing. Measure before keeping it: on a 1:64 part a few FP64 operations per element can cost more than the registers buy back. The macros change the bodies of inline functions, so set them for the whole build rather than for one file, and they affectfp32mp2only; anfp64mp2conversion is FP64 either way.CCCL_FPMP_EXPLICIT_CASTS=0is for the first compile of a migration, not for the build you ship. The default makes every narrowing conversion into the pair a visible cast; switching it off lets a largedoublecodebase compile againstfp32mp2without touching the call sites. The conversions still take the accurate two-limb path, so what is saved is edit churn rather than correctness - but the diagnostics it silences are exactly the list the second point says to work through: every place adoubleenters a pair expression and a conversion runs. With the macro off, adoubleintermediate in a hot loop, or a helper that still takesdouble, costs a conversion in each direction and FP64 arithmetic in between, with nothing to say so. The genuinely lossy conversions -int64_tandfp64mp2intofp32mp2- pass silently as well. Use it to get a large port compiling, then turn it back on and work through the errors; each one is a conversion site worth knowing about.Avoid the atomics, even though they are provided.
atomicAddandatomicSubonfp32mp2are a 64-bitatomicCASloop: each attempt reads the pair, performs ahighaddition and tries to exchange, and under contention a failed exchange means the whole addition is redone. HardwareatomicAddonfloatordoubleis one instruction resolved in L2. The gap is small while every thread hits a different address and large when many hit the same one, which is the reduction pattern. Reduce first: the__shfl_down_syncfamily is overloaded for the pair, so a warp reduces in five shuffle steps and a block finishes in shared memory, leaving one atomic per block - or none, with per-block partials and a second pass, which also makes the result deterministic. The type is trivially copyable with the usual operators, so generic block and warp reductions accept it as they do any user-defined type.Smaller things that add up:
Prefer the one-limb accumulate.
acc += fwith afloatoperand takes a dedicated path, 10 instructions against 11 atmidand 13 against 20 athigh, and builds no temporary pair.Prefer
madto a multiply and an add, and usefmafor its rounding, not by habit.mad(a, b, c)is alowmultiply followed by an addition at the type’s level, 16 instructions atmidagainst 20 for the two operations written out.fmais 19 atmidand 37 athigh, more than the separate operations athigh, so it belongs where the fused rounding of the main term matters.Hoist divisions. Division is the most expensive basic operation and the least accurate. A loop-invariant divisor is a reciprocal computed once and multiplied, at the price of one extra rounding, and where a square root feeds a division,
rsqrtand a multiply replace the pair.Mind the register count. Each pair is two registers, and the
highalgorithms carry more temporaries.-Xptxas -vor Nsight Compute shows spills;__launch_bounds__and the conversion macros above are the levers.The fast-math flags do not reach the arithmetic. The error-free transformations are written with the
_rnintrinsics -__fadd_rn,__fmul_rn,__fmaf_ieee_rnand theirdoublecounterparts - and the seeds with explicit approximation instructions, so--fmadcontraction and the--use_fast_mathdivision and square-root replacements do not alter them; flush-to-zero touches only the subnormal range, whichlowandmiddo not support in any case.Template the kernel on the type. The level is part of the type, so a kernel templated on it compares
low,mid,highanddoublefrom the build line with no change to the code, which is how the figures above were taken.
fp64mp2#
Use it only on GPUs with full-rate FP64.
fp64mp2spends FP64 throughput - 11 FP64 instructions permidaddition, 9 per multiplication - so the price of its digits is set by how the part rations that pipe. Where FP64 runs at half the FP32 rate, as on B200, H100 and A100, the price is fair: on B200 amidaddition sustains 1347 giga-additions per second and is 7.8× faster than software binary128, and division is 17.8× faster. On B300, with FP64 at 1/64 of FP32, the same addition sustains 45 - a thirtieth - and software binary128, which is integer code and does not compete for the FP64 units, is 1.4–4× faster thanfp64mp2operation for operation. On such a part the choice is usuallyfp128for the digits ordoublefor the speed, withfp64mp2the worst of both - usually, because those figures come from small kernels. Software binary128 is long integer code with many values live at once, and a large kernel already near its register budget can spill where the pair, at four registers per value and a dozen instructions per operation, does not; once the spills sit in the inner loop the ordering can reverse, andfp64mp2comes out ahead on rationed FP64 after all. The same code size shows up at build time, since everyfp128operation instantiated adds noticeably to compile time. So measure in the real kernel, with-Xptxas -von both builds, rather than from the table. The host side has no such rationing, so CPU code is on the good side of this line.Conversions are free; bandwidth and registers are not. Unlike the float pair,
fp64mp2converts to and fromdoubleat no cost worth measuring:doubleinto the pair is a limb copy with a zero tail, and the pair intodoubleis at most one FP64 addition, so the conversion macros above play no part here. What the wider type costs is 16 bytes per value against 8, and four registers against two. Keep data that isdoubleprecision to begin with indoublearrays, and promote where the extra width is needed - typically the accumulator, since the inputs are exact asdoubleand it is the running sum, or sum of products, that outgrows it.acc += dwith adoubleoperand is the cheap path for exactly that, 10 FP64 instructions atmidagainst 11 for a pair addition after promotion and 13 against 20 athigh. Convert the result back once, when it leaves the kernel. Where whole arrays must befp64mp2, the type is 16-byte aligned, so each element is one 128-bit load or store: keep them contiguous and aligned, and budget half the elements per byte of bandwidth. The exception to “free” is the quad interchange:fp64mp2↔__float128is software in both directions, about 1200 instructions on the device, and belongs at the I/O boundary.The accuracy levels follow the float pair, with one extra check. The costs are the same - 8, 11 and 20 FP64 instructions per addition - and so is the advice:
midby default,lowon the dominant path withrenormalize()where the chain ends,highper operation where cancellation is possible, andfp64mp2_statto find both. The extra check iszero_lo_count. The point of the type is digits beyonddouble; if that counter is a large share ofops_count, the second limb is idle for most of the data, the computation is not using what it pays for, anddoublewould give the same answer faster.Math functions are where the time goes. The arithmetic,
sqrt,rsqrt,fmaandmadhave dedicated double-double implementations; the transcendentals currently not. As Math function accuracy describes, they evaluate in a binary128 backend where one is active and indoublewhere not, and the two cases call for opposite handling.Division costs more than on the float pair. A
middivision is 18 FP64 and 25 integer instructions,high28 and 49, with the integer part doing the branch-free scaling that keeps reciprocals in range. On B200 amiddivision has 80% of the throughput of amidaddition and ahighdivision 44%, so the hoisting advice above applies with more force: reciprocals once,rsqrtand a multiply rather than one oversqrt.The atomics need sm_90 and are slower still.
atomicAddonfp64mp2is a 128-bit compare-exchange loop, which exists on Hopper and later only - elsewhere the call fails to link with an error naming the requirement - while hardwaredoubleatomicAddhas been one instruction since sm_60, so the gap to the native type is wider than for the float pair. The remedy is the same: shuffle and shared-memory reduction first, one atomic per block at most, or per-block partials and a second pass for a deterministic result.It is the type to port 80-bit
long doublecode to. CPU code written for the x87 double-extended format -long doubleon Linux/x86 - meets a GPU that has no such format in device code.fp64mp2carries 104 significand bits against that format’s 64, so the precision survives the port with room to spare, and on a full-rate FP64 part it is the cheapest way to carry it: a software binary128 would pay for 113 bits where 64 were asked for. What does not survive is exponent range,double’s ±10308 against x87’s ±104932, so code that chose the format for its range rather than its mantissa needs rescaling or binary128 - see the note on retiring 80-bit code under Where this pays off.Know when the alternative is better. Two cases call for something other than
fp64mp2even on a full-rate part. A computation that needs more than 104 significand bits, or a wider exponent range thandouble’s, needs binary128 and nothing else will do. And a computation whose only ill-conditioned part is a sum or a dot product may get its digits from compensateddoublearithmetic - Kahan, pairwise summation, the Ogita–Rump–Oishi dot product in the references - which is built from the same error-free transformations as the pair. Where only the accumulation needs the extra width,fp64mp2for the accumulator alone, as above, is that algorithm written for you, at 10 FP64 instructions per term.
Using fpemu efficiently#
fpemu is double built from 32-bit integer and float operations. Its costs are the pack
and unpack at the boundaries and the integer work inside, and almost everything below is about
keeping the first out of the loop and matching the second to what the computation needs. The
measured figures live on the fpemu page; the advice here
is qualitative.
The unpacked form can beat native
doubleon both speed and accuracy; the packed form rarely does in real applications.fp64emu_unpackedpays the pack and unpack once per chain rather than once per operation and keeps 9 guard bits between operations, so on a rationed part a chain of it runs ahead of hardwaredoubleand lands closer to the exact answer. That is the form that competes with nativedoublein real applications, and sometimes wins. Packedfp64emuis a different proposition. The π integration on the fpemu page puts it close to parity with hardwaredoubleon RTX 6000 Ada, RTX PRO 6000 Blackwell and B300, with the unpacked form at about 1.5× it, but that kernel is the exception rather than the rule: a few operations per term, no memory traffic, nothing else competing for registers. A real kernel is larger and rarely bound by arithmetic alone, and each emulated operation replaces one FP64 instruction with a run of integer and FP32 instructions and several more live registers, so in a large kernel the packed form usually means register pressure, spills and a net loss against the FP64 pipe, slow as that pipe is. Its uses are the ones the unpacked form cannot serve:doublework moved onto the FP32 and integer pipes alongside a saturated FP64 pipe, see Mixed cases; results that must matchdoublebit for bit while the FP64 pipe is avoided; and a target with no FP64 arithmetic at all. On a full-rate part hardwaredoubleis the fast path for everything, and emulating it only slows the mix down.Know what the default buys.
fpemudefaults tohigh, the opposite offpmp, so packedfp64emureproducesdoublebit for bit out of the box - and at best runs at aboutdouble’s speed. That is the right type when the requirement is exactdoublesemantics off the FP64 pipe, and the wrong one when the requirement is speed: for speed, step up to the unpacked form, step down a level, or both.Chains go unpacked. Everything the unpacked form gains, in speed and in accuracy, comes from paying the pack and unpack once per chain instead of once per operation, and from the 9 guard bits that survive between operations. Enter it once, leave it once, and keep the intermediate values in it. The explicit constructor is the reminder: every
fp64emu_unpacked{x}in a loop body is a repacking that was probably avoidable.Budget 16 bytes per unpacked value. Four registers rather than two, and twice the bytes if it is ever stored. Store packed, compute unpacked, and watch
-Xptxas -vin kernels with many live values; the unpacked form is the one that costs occupancy.Step down with the range in mind.
midis 1–2 ulp andlowaboutfloat’s precision;midruns at 1.4–1.6×doublepacked and 1.8–2.0× unpacked,lowat 1.7–2.0× and 2.0–2.3×. In the packed form both handle the normal range only and drop the subnormal, infinity and NaN handling thathighcarries; the unpacked form keeps full-range pack and unpack at every level, so stepping down there is the safer move.low’s error is biased rather than scattered - a long accumulation inlowtracks plainfloat- so its place is a range-limited loop body with a wider accumulator elsewhere, not the accumulator itself.Use the fused operations.
fma,mad,dotandcmulexist because composing them from*and+would round and repack between the steps. Where an expression has one of those shapes, call the fused form.Directed rounding is a packed-only feature. The
rz,ruandrdspellings of the intrinsics exist for the packed representation; the unpacked form rounds to nearest and nothing else. Interval arithmetic and its relatives therefore stay packed.Decide what “reproduces double” means before choosing. Only packed
highreturnsdouble’s values. A chain in the unpacked form behaves like x87 extended precision stored at the end: more accurate, and different. Code validated bit for bit against adoublereference needs the packed form, or a reference computed the same way.
Using fptool#
The two instruments answer the two questions the previous sections keep raising: how much
precision does this computation need, and what is the pair arithmetic actually doing. Both are
opt-in behind CCCL_ENABLE_FPTOOL, defined for the whole build; both cost rather than save; and
both are used the same way - change a type alias, read the answer, change it back. The macro is
there because both tools keep their state in global memory and touch it from inside the arithmetic:
fp64_custom reads its run-time field sizes there, and fpmp2_stat writes its counter record
there through atomics on every operation. Global objects reached that way can cause problems in a
complex parallel application, so the gate makes sure they leave the target application together with
the research work that brought them in rather than staying behind.
The fp64_custom page and
the fpmp2_stat page describe them; this section says
how they fit into an optimization pass.
fp64_custom: how much precision is enough#
fp64_custom<E, M> is a double that rounds to M mantissa bits and E exponent bits
before and after every operation, so an algorithm can be run as though the hardware had that format.
It is the tool for the second question in
Choosing the type.
Sweep the mantissa with the range held native.
fp64_custom<11, M>reduces the significand and clamps nothing, so a sweep overMshows where the computation itself gives way. The run-time sizes,fp_custom_dynamic_sizewith the setters, make the sweep one binary and one loop; the compile-time sizes make the native format cost nothing.Keep the reference reduction wider than the format under test. Reduce the per-thread partials in
fp64mp2or the like, so the sweep measures the loop rather than the summation. That gives the best result each format can produce, which is what a sizing question wants.Then sweep the exponent separately.
fp64_custom<E, 52>keeps the precision and narrows the range, which separates an algorithm that needs bits from one that needs headroom - the distinction betweenfp32mp2, which hasfloat’s range, andfp64emu_low, which hasdouble’s.Know what is not reduced. Construction stores the value unreduced, comparisons compare unreduced bits, and every math function except
sqrtandfmaresolves throughoperator double()and computes at full FP64 precision, silently. A sensitivity study that leans onexporfabsis measuring less than it thinks.It is cheap to run. Every reduced format costs about 10% over plain
doubleon the π integration, so a full sweep is an afternoon’s measurement rather than a project. The run-time sizes pay a memory read per reduction, and under whole-program compilation each translation unit has its own copy of them, so a size set in one is not seen by the others; relocatable device code shares them.
fpmp2_stat: what the arithmetic did#
fp32mp2_stat and fp64mp2_stat wrap the fpmp2 types, compute bit-identical results, and
record on the device how many operations of each kind ran, which numerical events occurred and how
the limbs were placed. Several points in
Using fpmp efficiently lean on them;
this is the order to read the record in.
Validate
ops_countagainst a closed form. Most kernels have one. A count that disagrees means the region is not the one intended, something was optimized away, or part of the arithmetic ran on the host, where nothing is collected.Read
zero_lo_countas a share ofops_count. A large share means the second limb was idle for most of the data and the pair type is not earning its cost -fp32mp2might be onlyfloat’s job (fp64mp2double’s).Compare
_low,_midand_highrecords on the same workload.overlap_count,invert_countandmin_hi_lo_gapon the_lowrecord say whether the fast level is safe on this data and how far the limbs drift, which is where to putrenormalize(). Theinvert_countmust be zero at every level - important.Read the cancellation counters as locations, not damage.
partial_cancel_countandfull_cancel_countmark the operations where significance was lost, which is wherehighpays and where a reformulation might. A compensated sum cancels on every term while being at its most accurate, so the count is a map of where to look rather than an error count.Check the exponent range.
min_exp,max_expanddenorm_countsay whether the data approaches the edges:fp32mp2inheritsfloat’s range, and thelowandmidlevels support the normal range only, so any subnormal means a configuration is being used outside its domain.
Instrument one variable and leave the rest plain - the property spreads along an expression from
either operand until a plain variable absorbs it, so the counters describe that variable’s
arithmetic rather than the kernel’s. Run on a reduced problem with a small grid, one block of 32
threads being enough, since every operation updates one device-wide record through atomics and an
instrumented run is two to three orders of magnitude slower than the plain type. Bracket the region
tightly with fpmp2_stat_reset_device_data and fpmp2_stat_read_device_data, measure one type
at a time because all eight aliases share the record, and keep the alias behind a build flag so the
instrumentation cannot ship.
Measuring#
Every recommendation above ends in the same place: the figures in these documents are one benchmark on a handful of parts, and the computation that matters is yours. The component types are built to make that measurement cheap.
Template the kernel on the type. The level, the width and the component are all part of the type, so a kernel templated on it compares
float,double, everyfpmpandfpemuvariant and thefptoolinstruments from the build line with no change to the code. One alias is the whole configuration, and the π integration behind the figures in these pages is written that way.Measure end to end, with the accumulator where it lives. Both the
fpmpand thefpemumeasurements show the reduction deciding most of the accuracy column. A measurement that reduces in a wider type than the one under test reports the loop’s accuracy; one that reduces in the same type reports the program’s. Neither is wrong, but only one matches the program you are going to ship.Report digits and time together. A faster type that loses the digits the computation needed has not won, and a slower one that gains digits the computation cannot use has not either. The tables in the component pages pair the two columns for that reason; a measurement of your own should do the same.
Check the pipe you meant to use. For an
fp32mp2orfpemukernel on a rationed part, the loop body should carry no FP64 instructions - thefp32mp2conversions keep a few on a cold fallback path, and that is all:cuobjdump --dump-sass kernel.o | grep -cE 'DADD|DMUL|DFMA|DSETP|F2D|D2F|F2F\.F64|F2F\.F32\.F64'
A count beyond those few points at a hidden conversion - a
doublevariable in a pair expression, adoubleliteral that did not fold, a helper that still takesdouble, orCCCL_FPMP_EXPLICIT_CASTS=0letting one through - and the same count on anfp64mp2kernel says how much of the FP64 pipe each element costs.Check registers and spills.
-Xptxas -vreports both per kernel.fp64mp2andfp64emu_unpackeddouble the register footprint of the value they replace, and thehighalgorithms carry more temporaries. A kernel that spills is usually a kernel to restructure before it is a kernel to speed up, and it is also the one case where software binary128 andfp64mp2can trade places on a rationed part.Compare against the type you are replacing, on the same part. The question is never whether
fp32mp2is fast but whether it is faster thandoubleon the GPU the program runs on, and the ratios in these documents move by an order of magnitude between a 1:2 and a 1:64 part. Measure the baseline in the same run.