Skip to content

fuzz: differential fuzzer + workarounds for the two Espresso bugs it found (fixes #112, #113) - #114

Merged
sbryngelson merged 2 commits into
mainfrom
fuzz/harness
Jul 23, 2026
Merged

fuzz: differential fuzzer + workarounds for the two Espresso bugs it found (fixes #112, #113)#114
sbryngelson merged 2 commits into
mainfrom
fuzz/harness

Conversation

@sbryngelson

@sbryngelson sbryngelson commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Two commits: the fuzzer, and the fixes for what it caught on its first run.

The fuzzer

Designed after reviewing NNSmith-style DNN-compiler fuzzing and EMI/metamorphic compiler testing, specialized to this hardware: an integer-exact bit-equality oracle (fp16 is exact on integers ≤2048; closure survives rewrites, enabling exact EMI probes of the canonicalizer), boundary-lattice sampling (powers-of-two ±1, tile factors, danger values), a conditioning-screened float mode (fp32-vs-fp64 mirror agreement gates judgment), and a contract-aware oracle (opt=0 exact; opt=1 answers to the autotuner's accuracy gate — verified int8-variant signature). Kind-preserving shrinking to minimal replayable JSON specs; scripts/fuzz.sh community wrapper; host-side machinery CI-tested off-device.

What it caught, root-caused to the MIL level

Both bugs receive correct MIL and produce wrong programs — Apple's Espresso/ANECompiler, not ANEForge's emission:

  • floor/ceil/round silently drop a following scalar multiply (opt=0) #112floor/ceil/round + scalar multiply: Espresso fuses the scalar into the rounding op's scale slot and the kernel drops it. Probed boundary: full-shape const blocks the fusion, (1,1) broadcast doesn't, scalar adds honored, no other op family affected. Emitters now materialize a full-shape constant after rounding ops (muls and binary-mul-with-size-1-const paths).
  • softplus returns 0 for inputs >= 11 (fp16 exp overflow in the lowering) #113softplus(x≥11.09) = 0 (fp16 exp overflow inside the lowering; 11.09 = ln(65504)). Emitter now lowers softplus(min(x,10)) + relu(x−10) — never overflows, error ≤ 4.5e-5, verified across ±30000.
  • Bonus semantics finding: ANE round is half-away-from-zero (np.round is banker's) — mirror and tests corrected.

Verification (on-device, M5)

  • Both original finding specs replay as PASSING post-fix (--repro).
  • 7 new regression tests in tests/test_espresso_workarounds.py; 180 tests across the ONNX/fuzz/dsp suites; off-device selection unchanged; ruff/pylint/pyright clean.
  • A fresh 400-graph batch produced 7 new findings (incl. Espresso internal crashes — unordered_map::at — on valid MIL); triage is follow-up work, which is rather the point of the tool.

…nd two real bugs

Three design ideas fitted to this hardware (after a review of NNSmith-style
DNN-compiler fuzzing and EMI/metamorphic compiler testing):

1. Integer-exact mode: fp16 is exact on integers <= 2048 and a large op
   subset is closed over them, so graphs whose numpy mirror proves every
   intermediate integral get a BIT-EQUALITY oracle - no tolerances, no
   false positives. Integer closure survives rewrites, so an
   identity-augmented EMI variant probes the canonicalizer exactly.
2. Boundary-lattice sampling: dims and params drawn from the edges where
   lowering bugs live (powers of two +/-1, tile factors {2,3,4,8},
   nonzero last-axis slice offsets, fp16 danger values).
3. Float mode with a conditioning screen: the mirror runs at fp32 AND
   fp64, and graphs where those disagree are discarded as ill-conditioned
   rather than judged.

The oracle honors the compiler's contract: opt=0 answers exactly (int
mode), opt=1 answers to the autotuner's accuracy gate, since gated lossy
variants (int8 weights) are by design - the first fuzz batch taught us
that distinction by flagging them. Failures shrink to minimal reproducers
(kind-preserving, invariant-respecting) with replayable JSON specs and
fingerprint dedupe; scripts/fuzz.sh is the community wrapper with a
paste-ready report.

The first corrected 150-graph batch (master seed 42, ~10s on an M5) found
two real silent-wrongness bugs, filed with hand-minimized repros:
floor/ceil/round dropping a following scalar multiply (#112) and softplus
returning 0 for inputs >= 11 = ln(fp16_max) (#113).

Host-side machinery (generator, mirrors, shrinker) is CI-tested off-device
in tests/test_fuzz_harness.py; one requires_ane smoke test runs the full
loop on hardware.
#113)

Both bugs receive CORRECT MIL and produce wrong programs, so the fix is
to route the emitters around the fusion:

- floor/ceil/round followed by a SCALAR multiply: Espresso fuses the
  scalar into the rounding op's scale slot and the kernel drops it
  (floor(x)*k returned floor(x)). Probing showed a FULL-shape constant
  blocks the mis-fusion while a (1,1) broadcast does not, scalar adds are
  honored, and no other op family is affected. The muls emitter and the
  binary-mul-with-size-1-const path now materialize a full-shape constant
  after rounding ops.

- softplus: the ANE lowering computes exp(x) in fp16 and returns 0 for
  every x >= ln(65504) ~= 11.09 (sp(10)=10, sp(11)=0 - silent). The
  emitter now lowers the stable split softplus(min(x,10)) + relu(x-10),
  whose exp argument never overflows and whose error is <= 4.5e-5.

Also fixed in passing, discovered by the regression test: ANE round is
half-AWAY-FROM-ZERO while np.round is banker's half-to-even - the fuzzer
mirror and the test reference now model half-away.

Verified: both fuzzer finding specs replay as PASSING; 7 new regression
tests (rounding x {muls, const-mul, adds}, softplus sweep incl. 30000 and
the normal range); 180 tests across test_onnx/test_fuzz_harness/
test_dsp_welch; off-device selection unchanged. A fresh 400-graph fuzz
batch found 7 NEW findings (incl. Espresso internal crashes on valid
MIL) - triage tracked separately.
@sbryngelson sbryngelson changed the title fuzz: differential fuzzer with an integer-exact oracle — first run found two real bugs (#112, #113) fuzz: differential fuzzer + workarounds for the two Espresso bugs it found (fixes #112, #113) Jul 23, 2026
@sbryngelson
sbryngelson merged commit 85679e5 into main Jul 23, 2026
13 checks passed
@sbryngelson
sbryngelson deleted the fuzz/harness branch July 23, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant