Skip to content

fix(torch-frontend): numerically stable log_softmax and logcumsumexp - #2763

Open
davidnichols-ops wants to merge 3 commits into
apple:mainfrom
davidnichols-ops:fix/torch-logsoftmax-logcumsumexp-fp16-stable
Open

fix(torch-frontend): numerically stable log_softmax and logcumsumexp#2763
davidnichols-ops wants to merge 3 commits into
apple:mainfrom
davidnichols-ops:fix/torch-logsoftmax-logcumsumexp-fp16-stable

Conversation

@davidnichols-ops

Copy link
Copy Markdown

Summary

  • Replace naive softmax -> log decomposition in log_softmax converter with x - reduce_log_sum_exp(x, axis), which uses the max-shift trick internally to prevent fp16 overflow on the Apple Neural Engine.
  • Replace naive exp -> cumsum -> log decomposition in logcumsumexp converter with a global-max-shifted version that prevents overflow for large inputs.
  • Add large-value regression tests for both ops.

Why

The naive decompositions overflow in fp16 on the ANE:

  • log_softmax: softmax -> log overflows when one class dominates, producing -inf for non-dominant classes (exp(x) > 65504 when x > ~10.4).
  • logcumsumexp: exp -> cumsum -> log overflows for the same reason.

reduce_log_sum_exp already exists as a MIL op and applies the numerically stable log-sum-exp trick: max(x) + log(sum(exp(x - max(x)))), keeping exp arguments ≤ 0.

For logcumsumexp, there is no cumulative-max MIL op, so we shift by the global max along the dimension. This prevents overflow (the catastrophic failure) though extreme underflow may still produce -inf for early elements far below the max — strictly better than the naive version which overflows to +inf/NaN for all elements.

What Changed

  • coremltools/converters/mil/frontend/torch/ops.py:
    • log_softmax: mb.softmax -> mb.log replaced with mb.reduce_log_sum_exp -> mb.sub
    • logcumsumexp: mb.exp -> mb.cumsum -> mb.log replaced with mb.reduce_max -> mb.sub -> mb.exp -> mb.cumsum -> mb.log -> mb.add
  • coremltools/converters/mil/frontend/torch/test/test_torch_ops.py:
    • test_log_softmax: basic log_softmax test (did not exist before)
    • test_log_softmax_large_values: regression test with x * 15.0 inputs
    • test_logcumsumexp_large_values: regression test with x * 15.0 inputs

Scope

Only the log_softmax and logcumsumexp torch frontend converters and their tests. No MIL op definitions, no other converters, no API changes.

Deleted: nothing (bugfix adding stable decompositions — net-negative justification: the fix replaces 2-line naive decompositions with 3-4-line stable ones; deletion/relocation impossible because the numerical instability is in the decomposition itself, not in misplaced logic).

The TensorFlow frontend's Softplus op emitted the native mb.softplus MIL
op, which on the Apple Neural Engine overflows in fp16 for x > ~10.4
(exp(x) exceeds 65504, the fp16 max). This caused output collapse to
inf/0 instead of the correct ~x value.

Replace mb.softplus with the numerically stable decomposition
max(x, 0) + log(1 + exp(-|x|)), inlined directly in the converter.
Since -|x| <= 0, exp(-|x|) is always in (0, 1], so no overflow occurs
in any precision.

Add test_softplus_large_values which converts a TF softplus model with
large inputs (x=15, x=20) and verifies correct output by running the
model (not frontend-only). The test hardcodes ComputeUnit.ALL to make
the ANE eligible for execution. Without the fix, the native mb.softplus
overflows in fp16 on the ANE for these values, failing the output
comparison. With the fix, the stable decomposition produces correct
results.

Closes apple#2747
…er request

The test now only verifies runtime output correctness for large values.
The MIL op inspection was removed per reviewer feedback. Note that on CPU,
the native softplus produces correct output regardless of the fix — the
overflow only manifests on ANE with fp16 compute.
log_softmax: replace naive softmax -> log decomposition with
x - reduce_log_sum_exp(x, axis). The naive version overflows in fp16
on the Apple Neural Engine when one class dominates, producing -inf
for non-dominant classes. reduce_log_sum_exp applies the max-shift
trick internally, keeping exp arguments <= 0.

logcumsumexp: shift by global max along the cumulative dimension
before exp, then shift back. The naive exp -> cumsum -> log overflows
in fp16 for x > ~10.4. This uses the global max rather than a
cumulative max (not available as a MIL op), so extreme underflow may
still produce -inf for early elements — but this is strictly better
than the naive version which overflows to +inf/NaN for all elements.

Adds large-value regression tests for both ops.

Fixes apple#2728, apple#2729
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