# Apple MLX: переполнение в вещественных производных arcsinh/arccosh

Локальный технический отчёт, 10 сентября 2026 года.
Подтверждено на установленном MLX 0.32.2 и настоящем C++ runtime на CPU.
Это один общий дефект двух функций, а не 188 независимых ошибок.

## Воспроизведение

При конечном входе прямое значение функции остаётся конечным, но JVP
и VJP обнуляются. Математические производные представимы в выбранном dtype.

| Функция | dtype | Вход | JVP/VJP MLX при весе 1 | Производная |
|---|---|---:|---:|---:|
| arcsinh | float16 | 1000 | **0** | 0.000999999500000375 |
| arccosh | float16 | 1000 | **0** | 0.001000000500000375 |
| arcsinh | float32 | ≈1e20 | **0** | ≈1e−20 |
| arccosh | float32 | ≈1e20 | **0** | ≈1e−20 |
| arcsinh | float64 | 1e200 | **0** | 1e−200 |
| arccosh | float64 | 1e200 | **0** | 1e−200 |
| arcsinh/arccosh | bfloat16 | ≈9.97277e19 | **0** | ≈1.00273e−20 |

Для отрицательного большого входа arcsinh тоже теряет положительную
производную. Для float16 при x=1000 обе прямые функции возвращают
7.6015625, то есть переполняется именно промежуточное вычисление производной.

Минимальное воспроизведение:

    import mlx.core as mx
    mx.set_default_device(mx.cpu)
    x = mx.array(1000., dtype=mx.float16)
    print(mx.arcsinh(x))                          # 7.6015625
    print(mx.grad(mx.arcsinh)(x))                 # 0, должно быть около 0.001
    print(mx.jvp(mx.arcsinh, [x], [mx.ones_like(x)])[1])

[Полный пробник](probe.py), [измерения установленного wheel](wheel-reproduction.json).
Эталон вычисляется после приведения входа к его фактическому dtype:
1/hypot(x,1) для arcsinh и (1/sqrt(x−1))/sqrt(x+1) для arccosh.

## Контроль через составную функцию

Для f(t)=arcsinh(Ct) или arccosh(Ct), t=1, использованы C=2⁸⁰
в float32 и C=2⁶⁰⁰ в float64. Все прямые значения конечны.

| Проверка | Исходный MLX | После локального патча | Аналитический эталон |
|---|---:|---:|---:|
| Первая производная | 0 | 1 | ≈1 |
| Вторая производная | −0 | −1 | ≈−1 |
| Третья производная | NaN | 2 | ≈2 |

Поправки конечного C к правому столбцу меньше отображаемой точности.
Центральная разность **прямой функции настоящего MLX** даёт
1.0001220703125 в float32 при шаге 2⁻⁶ и 1.00000031790114 в float64
при шаге 2⁻¹⁰. Она независимо подтверждает первый градиент порядка единицы.
Вторая и третья производные проверяются аналитическими формулами.

Это также показывает, почему потерю малого локального множителя нельзя
списывать на малую абсолютную ошибку: в композиции ошибка градиента равна
примерно единице. Влияние на обучение конкретной модели не измерялось.

## Причина и исследовательский патч

Публичные методы
[ArcCosh::jvp](https://github.com/ml-explore/mlx/blob/81ba1c6a0e50a9268b931579c2d4f1158b9aab5a/mlx/primitives.cpp#L424)
и
[ArcSinh::jvp](https://github.com/ml-explore/mlx/blob/81ba1c6a0e50a9268b931579c2d4f1158b9aab5a/mlx/primitives.cpp#L478)
вычисляют rsqrt(x²−1) и rsqrt(x²+1) соответственно.
Оба VJP делегируют JVP. Если x² переполняется, rsqrt(inf) становится
нулём, хотя итоговая производная не должна исчезать.

[Патч](inverse-hyperbolic-real.patch) вводит общую нормировку только
для вещественного dtype. При s=max(1,|x|) он использует тождества

    arcsinh'(x) = rsqrt((x/s)² + (1/s)²) / s,
    arccosh'(x) = rsqrt(((x−1)/s)·((x+1)/s)) / s,  x>1.

Квадрат большого x не формируется. Разложение x²−1 на множители также
устраняет вычитание близких квадратов около x=1. Например, при
float32 x≈1.00010001659 исходная производная 70.7048111 отличается
от эталона 70.7030442. Это смежный эффект той же формулы.

Масштаб s помечен stop_gradient. Это не отбрасывает математическую
зависимость производной: каждое тождество справедливо для любого
фиксированного положительного s и всех x в области определения.
В окрестности точки можно выбрать такое постоянное s и дифференцировать
тождество по x. При этом лишние пути производной через выбор масштаба
не создают опасных промежуточных квадратов знаменателя.

Для неконечных входов код выбирает s=1, чтобы не вводить inf/inf
самой нормировкой; полноценная проверка NaN/±inf в этот пакет не входит.
Комплексные ветви JVP и тела VJP сохранены. Прежние известные проблемы
комплексных производных этим патчем не исправляются.

Это вариант исправления для проверки корректности. Стоимость
дополнительных операций, compile, GPU и пригодность для включения
сопровождающими проекта не оценивались.

## Нативная проверка

[Тест C++](native_regression.cpp) выполняет реальные операции MLX
до и после замены одной единицы трансляции, mlx/primitives.cpp.

| dtype | Проверки | Несовпадения до | Несовпадения после |
|---|---:|---:|---:|
| float32 | 195 | 73 | 0 |
| float64 | 195 | 67 | 0 |
| float16 | 64 | 24 | 0 |
| bfloat16 | 64 | 24 | 0 |
| Всего | **518** | **188** | **0** |

- Float32/float64: нулевые, малые, умеренные и большие аргументы,
  ближайший представимый x>1 для arccosh; направления 0,1,−0.5,2.
- Вторые и третьи производные на умеренных входах и композициях
  с большим C, включая отрицательный t для arcsinh.
- Плотная и неплотная матрица 2×3 со смешанными весами.
- Float16/bfloat16: первый порядок JVP/VJP, умеренные и большие входы.
  Высшие производные и матрицы для этих двух dtype не проверялись.

Допуск относительный при ненулевом эталоне: 3e−13 в float64,
8e−6 в float32, 3e−3 в float16, 2e−2 в bfloat16.
При нулевом эталоне используется тот же численный абсолютный допуск.
Потеря любой из проверенных ненулевых производных в ноль означает
относительную ошибку 1 и не может скрыться за допуском.

[Результаты до](run-before.json), [после](run-after.json),
[скрипт сборки](build_and_test.py), [команды и время](build-results.json).
Конечная серия не доказывает корректность всех входов и всех порядков.

В первом запуске C++-теста была обнаружена ошибка стенда:
array(double) по умолчанию создаёт float32, а последующий astype(float64)
не восстанавливает потерянную точность/диапазон. Из-за этого выбранный
double-масштаб уже был inf, а ближайшее double-число к 1 стало 1.
Исходные 390 результатов, включая 31 несовпадение после патча,
сохранены в [initial-harness-attempt](initial-harness-attempt/).
Исправлено создание скаляров: array(value,dtype) сразу нужного типа.
Патч производной и допуски float32/float64 при этом не менялись.
Повторная серия дала 390/390, затем проверка двух дополнительных dtype
расширила её до 518/518. Начальный запуск не используется как
доказательство дефекта или корректности double-режима.

## Версии и ограниченный поиск дубликатов

- Установленный wheel: MLX 0.32.2, CPU.
- Нативная база: ce916dbbcaa88e433b6fd1e60a17f766d49c27fe.
- Публичный main: 81ba1c6a0e50a9268b931579c2d4f1158b9aab5a.
- Все четыре целевых метода JVP/VJP базы совпадают с сохранённым main.
- [Публичные источники и SHA-256](source-metadata.json);
  [нативные зависимости и граница сборки](native-source-metadata.json).

Все четыре запроса GitHub API выполнены, ответы complete.
По arcsinh найдены три записи, по arccosh две; сочетания
asinh+overflow и acosh+gradient не дали совпадений.
[Сохранённые ответы](duplicate-search.json).

- [#4227](https://github.com/ml-explore/mlx/pull/4227) предлагает тесты
  производных на умеренных входах и сообщает об отсутствии найденных
  дефектов в своей серии. В его описании нет текущего контрпримера.
- [#3678](https://github.com/ml-explore/mlx/pull/3678) добавляет имена
  asinh/acosh как алиасы, а не исправляет производную.
- [#3080](https://github.com/ml-explore/mlx/issues/3080) касается
  JIT-компиляции Float16 на arm64 Linux.

В прочитанных описаниях точного дубликата не найдено.
Это не доказательство приоритета и не полный поиск по всем обсуждениям.
Security-impact и право на оплату не установлены.

## Воспроизводимость и нагрузка

Нативный запуск использует существующий CPU-архив и отдельный объект
primitives.cpp перед ним при линковке. Это не полная чистая сборка
текущего main. Скрипт требует готовых зависимостей по путям из метаданных;
автоматической установки и полной пересборки нет.
Рабочая копия MLX с прежними изменениями не редактировалась.

Компиляции и тесты выполнялись последовательно; численные потоки
ограничены одним. GPU-вычислений не было. Для импорта установленного
wheel потребовалась системная инициализация Metal, после импорта
устройство явно установлено в CPU; нативный архив без Metal.
Последние два нативных тестовых запуска вместе заняли около 0.129
секунды CPU; компиляции и линковки — около 2.37 секунды CPU.
Финальный wheel-пробник — около 0.006 секунды CPU.

[Проверка целостности пакета](validate_artifacts.py) и
[её результаты](artifact-check-results.json).
Примечание: это исходный локальный отчёт до оформления публикации. Актуальный статус приведён в README.md.
