Accumulation Simulator

A bar-chart visualisation for accumulator precision in mixed-precision training. The component walks the first currentStep entries of values, accumulates them at the chosen precision, and compares the running total to the exact fp64 sum of the same prefix. Two stacked bars share a scale; a mono drift readout prints the signed delta between them, switching to cb-warning once the relative drift crosses one part in a thousand.

The teaching moment: summing many small values into one large one is lossy in low precision. Once the running sum grows past the format's spacing at that magnitude, each subsequent small addend rounds to zero. fp16 drifts visibly off the truth long before the sum reaches 1.0; fp32 holds — which is why mixed-precision training keeps accumulators in fp32 even when activations and weights are fp16. Sibling to NumberLineZoomer and BitFieldExplorer in the Numerics group.

Accumulation simulator (1000 of 1000 steps). fp16 sum 0.9785; exact sum 1; drift 0.0215.
Accumulation · fp16step 1000 / 1000
fp16
0.9785
exact
1
drift
-0.0215
Customize
Sequence
1000
0.0010
1000
Display
fp16

Installation

npx shadcn@latest add https://craftbits.dev/r/accumulation-simulator.json

Usage

import { AccumulationSimulator } from "@craft-bits/core";
 
<AccumulationSimulator defaultPrecision="fp16" defaultCurrentStep={1000} />

Drive precision externally:

<AccumulationSimulator
  precision={precision}
  onPrecisionChange={setPrecision}
  defaultCurrentStep={1000}
/>

Auto-play the accumulation:

<AccumulationSimulator
  defaultPrecision="fp16"
  playing
  playSpeed={60}
/>

Custom value sequence:

const values = Array.from({ length: 2048 }, (_, i) => 0.5 / (i + 1));
<AccumulationSimulator values={values} defaultPrecision="fp16" />

Anatomy

  1. Three precisions, one truth. fp64 uses the native JS Number and matches the exact reference. fp32 uses Math.fround — the canonical round-to-nearest-float32. fp16 is hand-rolled IEEE 754 round-to-nearest-even at a 10-bit mantissa. The exact reference sum is always computed in fp64 regardless of the prop, so the drift readout is unambiguous.
  2. Running sum, rounded at each step. Both the addend and the new running total are rounded to the chosen precision at every step. That mirrors the actual training loop — the gradient lands in the format first, then the add happens in the format — so the visualisation captures the failure mode (small delta below the spacing at the running total's magnitude) rather than a single end-of-loop rounding.
  3. Controlled or uncontrolled, twice. precision plus defaultPrecision and currentStep plus defaultCurrentStep follow the Radix idiom — pass the controlled prop alongside an on-change callback, or pass the default* prop alone. The internal playing loop honours whichever mode the consumer picked.
  4. Two bars, one shared scale. Both bars share a max of 1.1 times the larger of the two magnitudes so a small drift stays visible the instant it appears, but neither bar clips the readout. They animate via transform: scaleX so the spring is GPU-composited — never width, per the library's transform / opacity-only rule.
  5. SPRINGS.smooth for the accumulator. The simulated bar springs between values via SPRINGS.smooth; prefers-reduced-motion: reduce collapses it to an instant swap. The drift readout colour-codes ratios above 0.1% in cb-warning so the "your sum doesn't match the truth" moment is visible at a glance.

Props

PropTypeDefaultDescription
valuesreadonly number[]1 000 entries of 0.001Sequence of small values to accumulate.
precision"fp16" | "fp32" | "fp64"Controlled accumulator precision. Pair with onPrecisionChange.
defaultPrecision"fp16" | "fp32" | "fp64""fp16"Uncontrolled initial precision.
onPrecisionChange(precision) => voidFires when the user picks a different precision.
currentStepnumberControlled prefix length, clamped to 0 to values.length.
defaultCurrentStepnumber0Uncontrolled initial prefix length.
onCurrentStepChange(step: number) => voidFires whenever the prefix length changes.
playingbooleanfalseAuto-advance currentStep toward values.length.
playSpeednumber60Playback rate in steps per second when playing is true.
transitionTransitionSPRINGS.smoothOverride the spring for the simulated bar.
classNamestringMerged onto the root via cn().

Accessibility

  • The outer element is role="figure" with an aria-label describing the precision, the simulated sum, the exact sum, and the absolute drift.
  • An aria-live="polite" summary mirrors the same readout so screen readers hear the new values whenever the precision or step changes.
  • The precision picker is a role="radiogroup" of role="radio" pills with aria-checked. Tab focuses the group; Space and Enter commit a selection.
  • The step slider is a native <input type="range"> with Arrow / Home / End keyboard support and an aria-valuetext that spells out step N of M.
  • Colour is never the only signal — every readout has a textual label, and the drift cell only switches to cb-warning as an emphasis on top of the always-visible numeric value.
  • The bar animation respects prefers-reduced-motion: reduce and collapses to an instant swap when the user opts out.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/nn/AccumulationSimulator.tsx). The source bundled a five-phase narration state machine (observe / fp32-working / switch-low / stagnation / insight), a per-format "starting parameter" picker (FP32 / FP16 / BF16), an "Add Gradient" click loop, success / stuck / step counters, ghost bars rendered with imperative animate(...) calls, and the project's ChallengeBtn plus SvgLabel chrome. The library version reframes the same teaching surface as a pure accumulator drift visualisation — two bars (simulated + exact), three precisions (fp16 / fp32 / fp64), Radix-style controlled and uncontrolled prop pairs for precision and currentStep, a playing plus playSpeed loop for automatic sweeps, and cb-accent / cb-warning semantic tokens in place of the inline palette.