Почему PHP-массивы плохо подходят для математики, как появились Tensor и NDArray, и зачем RubixML в итоге решил пойти в сторону GPU.
1. Введение: а можно ли вообще делать ML в PHP?
Реакция на тему «машинное обучение на PHP» обычно предсказуема: усмешки, ссылки на Python или минусы. Вывод — это не та задача для этого языка.
В этом есть доля правды. PHP никогда не проектировался как платформа для численных вычислений. У него нет встроенной поддержки векторных операций, контроля над памятью или доступа к low-level оптимизациям, привычным в scientific computing.
Но машинное обучение в PHP существует. И не как эксперимент, а как часть реальных систем:
- inference прямо в веб-приложениях
- обработка данных внутри SaaS
- автоматизация и встроенная аналитика
И с каждым месяцем его использование растёт. Экосистема ML в PHP не шумная, но зрелая. Она состоит из четырёх слоёв:
- библиотеки классического ML
- математический фундамент
- инструменты интеграции с современными ML-системами
- интеграция с внешними ML-сервисами
В этой статье мы сосредоточимся на первых двух слоях.
Развитие экосистемы шло не благодаря языку, а вопреки его ограничениям — благодаря энтузиазму отдельных разработчиков.
Проблема не в алгоритмах, а в том, как устроена сама среда выполнения.
Сначала матрицы реализовывались как обычные массивы. Потом появились попытки оптимизации, затем — нативные структуры на C и Rust. И стало ясно: даже этого недостаточно.
Следующий шаг был неизбежен — вынос вычислений на GPU.
Мы пройдём этот путь: от наивных реализаций до современных решений вроде Tensor, NDArray и GPU-бэкендов в RubixML. И увидим, как менялось не только API, но и само понимание: где заканчивается PHP — и начинается настоящая вычислительная система.
Это не история о том, как делать ML в PHP. Это история о том, почему его пришлось делать по-другому.
2. Первая попытка: когда матрица — это просто массив
Матрица — это таблица чисел. А в PHP есть массивы. Значит, можно представить матрицу как массив массивов.
На этой идее строились первые библиотеки ML:
- PHP-ML — от Arkadiusz Kondas
- ранние версии RubixML — от Andrew DalPino
- и другие
Всё делалось в чистом PHP, без расширений и нативного кода. Вычисления выполнялись интерпретатором.
Для разработчика это было удобно: не нужно компилировать, можно дебажить привычными инструментами, код прозрачен.
Как это выглядело в коде
Операции вроде умножения матриц реализовывались «в лоб» — тремя вложенными циклами.
Скалярное произведение, прямой проход в нейросети — всё писалось вручную, без библиотек.
Этот подход всё ещё используется — в учебных целях, чтобы понять, как работают алгоритмы «под капотом».
Например, реализация forward pass одного слоя нейросети на чистом PHP показывает, что:
- подход остаётся понятным
- он полезен для обучения и экспериментов
Но при росте данных и в продакшене его ограничения становятся очевидны.
Почему это было удобно
Главное преимущество — простота. За вечер можно реализовать:
- k-NN
- линейную регрессию
- базовый классификатор
И всё — в рамках PHP. На маленьких данных это работает нормально. Для прототипов и обучения — более чем достаточно.
3. Почему это перестаёт работать
Сначала всё выглядит нормально. Потом данные растут — и код тормозит. Сначала чуть, потом в разы, а затем становится непригодным.
Первая мысль — «надо оптимизировать циклы». Но проблема не в коде. Она глубже — в устройстве самого PHP.
Число — это не просто число
В PHP число — это не примитив, а структура zval, которая хранит тип, значение и служебную информацию (например, счётчик ссылок).
Вместо 8 байт, как в C, каждый элемент занимает значительно больше. Миллион элементов — миллион структур zval.
Массив — это хеш-таблица
PHP-массив — это не непрерывная память, а хеш-таблица. Элементы не лежат подряд.
Каждый доступ включает:
- поиск ключа
- проход по внутренним структурам
- извлечение zval
Это серия операций, а не прямой доступ к памяти.
Из-за этого:
- хуже работает CPU cache
- доступ к данным дороже
- невозможна векторизация
Для численных вычислений это критично.
Copy-on-write
PHP может неявно копировать массивы при изменении. Мы не всегда контролируем, когда это происходит.
В задачах с матрицами это приводит к:
- резкому росту потребления памяти
- лишним аллокациям
Нет векторизации
Все операции выполняются через циклы. В NumPy та же операция — одна инструкция на уровне C с SIMD (Single Instruction, Multiple Data).
Алгоритм правильный, код простой — но каждая операция стоит слишком дорого.
Как это выглядит на практике
Сравним умножение матриц 500x500:
- PHP-массивы — ~10–20 сек
- Tensor (CPU) — ~0.3–0.8 сек
- GPU (NumPower) — ~0.05–0.2 сек
Цифры зависят от железа, но разница — на порядки.
Главное не абсолютные значения, а разница на порядки!
Дело не в том, что PHP «медленный». Дело в том, что он решает задачу не тем инструментом.
4. Попытки оптимизировать — и почему они не спасают
Можно оптимизировать циклы: вызывать count() один раз, использовать локальные переменные, минимизировать обращения к массивам.
Это даёт небольшой прирост, но быстро упирается в предел. Это оптимизация «на уровне синтаксиса».
Фундаментальные ограничения остаются:
- нет контроля над памятью
- нет contiguous storage
- нет SIMD
- нет BLAS
Узкое место — модель памяти PHP, а не реализация цикла.
Оптимизация: транспонирование
Один из трюков — предварительное транспонирование матрицы.
Проблема: при умножении $a[$i][$k] — это строка, а $b[$k][$j] — столбец. Доступ к столбцу в PHP неэффективен.
Решение: заранее транспонировать вторую матрицу. Теперь все обращения — к строкам.
Почему это быстрее:
- меньше «прыжков» по памяти
- лучше используется CPU cache
- доступ предсказуем
На практике это даёт заметный прирост даже в чистом PHP.
Но есть нюанс: мы всё ещё:
- работаем с zval
- используем хеш-таблицу
- остаёмся в интерпретаторе
Мы делаем вычисления чуть менее плохими, но не по-настоящему эффективными.
Даже продвинутые трюки на уровне PHP не заменят нормальную модель данных.
PHP просто не предназначен для численных вычислений.
Оптимизация: CPU-кэш
В C и NumPy важна cache locality — данные лежат подряд, CPU читает их блоками (cache lines).
Попытка оптимизации в PHP: писать код, идущий по памяти последовательно, избегая «прыжков».
Но в PHP это почти не работает. Даже «плоский» массив — это:
- не последовательность float-ов
- а массив zval
- внутри — всё ещё хеш-таблица
Результат:
- данные не лежат компактно
- между ними — служебные структуры
- CPU не может эффективно использовать cache
Мы пишем код так, как будто оптимизируем cache — но реальной выгоды почти нет.
Оптимизация: packed arrays
В PHP 7+ появились packed arrays — более компактное хранение для простых массивов.
Это ускоряет работу, но в задачах с матрицами почти не помогает. При вложенности:
- массив массивов
- непрерывность теряется
- каждый элемент — всё ещё zval
- contiguous memory нет
К тому же packed array легко «сломать» — например, удалением элемента. После этого PHP возвращается к хеш-таблице.
В ML-задачах такие операции — постоянны: фильтрация, reshaping, работа с индексами.
Мы можем немного ускорить код — но не можем изменить фундамент!
Проблема не в циклах — проблема в том, как данные представлены в памяти.
Неприметный аспект
Многие библиотеки того периода просто перестали развиваться. Интернет завален заброшенными PHP-проектами по ML.
Не потому что идея была плохой. А потому что разработчики упирались в один и тот же потолок.
Можно было переписать внутренности, добавить оптимизаций, улучшить API. Но кардинального прироста не было.
Сколько бы ни оптимизировали PHP-массивы, они не превратятся в структуру, подходящую для численных вычислений.
Оставалось два пути:
- переписывать на C / Rust
- потерять мотивацию
Часто происходило второе. Энтузиазм угасал, потому что каждый шаг давал всё меньший результат.
Дело было не в библиотеках — дело было в самой модели.
5. Поворот: когда матрицы перестают быть массивами
Следующий этап — отказ от идеи, что матрица должна быть PHP-массивом.
Появились библиотеки, хранящие данные в contiguous memory, как в NumPy:
- Rubix Tensor — от Andrew DalPino
- NDArray — реализация на Rust от Kyrian Obikwelu
Теперь:
- нет zval на каждый элемент
- нет хеш-таблицы
- данные лежат плотно
- CPU работает с ними эффективно
Код остаётся похожим, но внутри — совсем другое.
Интересно, что Rust используется как способ получить производительность без проблем C с памятью. Это пример того, как PHP делегирует тяжёлую работу другим языкам.
Ускорение — в разы. Но и этого оказывается недостаточно.
Как изменилась архитектура
Эволюция:
- раньше: PHP занимался вычислениями
- теперь: PHP управляет процессом (orchestration)
Код становится менее «PHP-шным» и ближе к математической модели.
Более сложные операции
Сложение, нормализация — теперь выглядят как математические формулы, а не набор циклов.
Но остаётся ощущение, что чего-то не хватает. API всё ещё «объектный», а не математический.
NumPower и новый уровень абстракции
Проект NumPower от Henrique Borba идёт дальше. Он не только ускоряет вычисления, но и меняет способ их записи.
Теперь можно писать:
- $a + $b * $c
- sin($x) ** 2
Это очень близко к NumPy. Это не синтаксический сахар — это попытка сделать математические выражения нативными для языка.
6. Новый потолок: когда CPU уже не справляется
Даже с идеальными структурами данных и нативным кодом CPU ограничен: мало ядер, низкая пропускная способность.
ML — это огромные матричные операции. Embedding-векторы, нейросети — быстро упираются в объёмы, где CPU сдаёт.
Следующий шаг — неизбежен.
7. Почему всё пришло к GPU
GPU заточен под параллельные вычисления. Тысячи потоков, высокая пропускная способность памяти — идеально для линейной алгебры.
Выжимать максимум из CPU — тупик. Нужно менять архитектуру.
Сейчас RubixML v3 движется в сторону GPU через проекты вроде NumPower.
Это смена парадигмы:
- PHP → orchestration
- Tensor / NumPower → вычисления
- GPU → heavy math
Проект в стадии активной разработки. Это момент, когда можно подключиться.
Это не просто «ускорим вычисления». Это переход на другую модель: PHP больше не считает — он управляет.
8. Что в итоге произошло
PHP использовался как вычислительная среда. Потом стало ясно: он для этого не подходит. Вычисления начали выноситься — сначала в нативные структуры, потом в расширения, теперь — на GPU.
Сегодня PHP в ML — это orchestration layer. Он:
- связывает компоненты
- управляет процессом
- обрабатывает данные
А тяжёлая математика — в других местах:
- GPU
- Rust
- нативные библиотеки
Пример — Transformers PHP от Kyrian Obikwelu. Это доступ к современным моделям в контексте PHP. Но под капотом:
- PHP управляет пайплайном
- загружает модели
- обрабатывает результаты
Вычисления — вне PHP: через нативные библиотеки или внешние рантаймы.
Похожую роль играет LLPhant — для LLM-сценариев:
- генерация текста
- embeddings
- retrieval (RAG)
- чат-интерфейсы
Код остаётся прикладным. Мы больше не думаем:
- о матрицах
- о циклах
- о памяти
Мы думаем:
- о данных
- о запросах
- о поведении системы
Появляются и более высокоуровневые инструменты. Например, Neuron AI — не про матрицы и модели, а про построение AI-приложений:
- агенты
- цепочки (chains)
- интеграции с LLM
Код ещё дальше от «низкого уровня».
Роль PHP изменилась:
- раньше — реализовывали математику
- потом — выносили её в нативные структуры
- теперь — описываем поведение системы
Весь путь:
- умножаем матрицы в массивах
- оптимизируем — упираемся в потолок
- выносим в нативные структуры
- в C / Rust
- и, наконец, на GPU
PHP не становится быстрее в ML. Он просто перестаёт считать.
Он больше не вычислительный движок — он диспетчер, оркестратор, клей между компонентами.
И, возможно, именно это и есть его шанс выжить в большой игре.
Но тогда остаётся вопрос: игра ещё идёт? Или PHP уже давно играет не в ту игру?